<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>jamieonkeys</title>
  <subtitle></subtitle>
  <link href="https://www.jamieonkeys.dev/feed.xml" rel="self"/>
  <link href="https://www.jamieonkeys.dev/"/>
  <updated>2026-09-22T23:00:00+01:00</updated>
  <id>https://www.jamieonkeys.dev</id>
  <author>
    <name>Jamie</name>
  </author>
  
    
    <entry>
      <title>Review: Roger Taylor rocks the Usher Hall</title>
      <link href="https://www.jamieonkeys.dev/posts/roger-taylor-usher-hall/"/>
      <updated>2026-09-22T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/roger-taylor-usher-hall/</id>
      <content type="html">
        <![CDATA[
      <figure class="w-950">
  <img src="/img/roger-taylor-usher-hall.jpg" width="950" height="662" alt="Photograph of Roger Taylor and band playing the Usher Hall, Edinburgh, in 2026.">
  <figcaption>Roger Taylor and band play the Usher Hall, Edinburgh, on 22 September 2026.</figcaption>
</figure>
<p>Besides being Queen’s drummer and writer of such hits as ‘Radio Ga Ga’ and ‘A Kind Of Magic’, Roger Taylor has released seven albums under his own name. While his solo songs might not be as well known to the average punter as those of bandmates Freddie Mercury (‘Living On My Own’, ‘Barcelona’) or Brian May (‘Driven By You’, ‘Too Much Love Will Kill You’), one could make the case that his oeuvre is the strongest of the three. He was the first in the band to release solo material, beginning with the single ‘I Wanna Testify’ in 1977 and then the album <em>Fun In Space</em> in 1981.</p>
<p>Taylor’s latest album, <a href="https://rogertaylor.lnk.to/VIIABW"><em>Violence Insane in a Beautiful World</em></a>, is the impetus for a short tour of the UK, which made a stop at Edinburgh’s Usher Hall last night (22 September). The show opened with a banger in ‘Strange Frontier’, one of his classic 80s tracks, bleakly anthemic and imbued with Cold War angst (‘Some day soon they’ll drop the big one / No more Dad and no more Mum’). This was followed by ‘Nation of Haircuts’, a satire of the superficiality of late-90s Britain (‘We don’t make ships and we don’t make cars, but we look real good hanging out in bars’). A strong start.</p>
<p>Taylor has always been a great singer, and he remains in fine voice, most evident last night on quieter numbers such as his lovely cover of John Lennon’s song ‘Jealous Guy’ (accompanied on piano by multi-instrumentalist Tina Keys). Some of the higher notes on older tunes like ‘I’m In Love With My Car’ and ‘Man On Fire’ might not be quite so readily attainable these days, but it would be unfair to expect any singer to have the same range as they did 40 or 50 years ago.</p>
<p>The new songs really shine live. ‘Violence Insane’ is the darkest thing he’s released since ‘Nazis 1994’, and it hit hard live, with its ominous pulsing beat, swirling synths and darkly distorted vocals. Its opposite number is ‘A Beautiful World’, featuring the Ndlovu Youth Choir from South Africa, written from the point of view of an alien looking down on Earth. ‘Don’t fuck it up,’ Taylor pleads. He took to the drum kit during this song and peppered fills over the gently shuffling groove.</p>
<p>Taylor has never shied away from penning songs about subjects and figures that irk him. Two numbers in this category (about ‘a couple of c**ts’) were featured in the show: ‘Dear Mr Murdoch’, originally from 1994, and ‘Chump’, from <em>Violence Insane…</em>. (Can you guess who the latter might be about?) To lift the mood, he followed with ‘Under Pressure’, the 1981 collaboration between Queen and David Bowie.</p>
<p>The other Queen tracks in the set list were ‘These Are The Days Of Our Lives’, ‘Rock It (Prime Jive)’ (sung by drummer Tyler Warren), ‘A Kind Of Magic’, and, to close, ‘Radio Ga Ga’. And from his solo catalogue: ‘Surrender’ (featuring a lovely vocal duet with Tina Keys), ‘I See You Now’, ‘Absolutely Anything’, ‘Don’t Photograph Food’, and ‘Come On Summer (It’s Party Time)’ (also featuring the South African choir, who were not present at the gig, but who <em>will</em> appear on stage at the London show on the 29th). Besides ‘Jealous Guy’, the evening’s other cover was Led Zeppelin’s ‘Rock and Roll’.</p>
<p>In conclusion, it would be fair to call his first show in Edinburgh since <a href="https://www.queenconcerts.com/detail/live/1021/roger-taylor-live-24-03-1999-the-liquid-rooms-edinburgh-uk.html">1999</a> a success. The <a href="/posts/roger-taylor-ticket-tracker-2026/">sell-out audience</a> was really into it, and Taylor and his band (which also includes Spike Edney on keyboards, Christian Mendoza on guitar and Neil Fairclough on bass) were on form and looked to be having fun.</p>
<p>It’s great to see that, after a career spanning 50+ years, Taylor is as creative and up for performing as ever. He doesn’t seem to have lost any of the drive and enthusiasm that sustained him during the early, lean years of Queen, when he scraped a living running a second-hand clothes stall with Freddie Mercury in London’s Kensington Market. He doesn’t need the money now, obviously, so it’s all about the music. And he might not be done yet: in an <a href="https://www.billboard.com/music/rock/queen-roger-taylor-new-album-1236343342/#:~:text=maybe%20there%20won%E2%80%99t%20be%20a%20next%20one%20%E2%80%94%20but%20I%20hope%20there%20will%20be">interview with Billboard</a> he said he hasn’t ruled out another album at some point in the future. Nice! Keep on rockin’, Rog.</p>
<h2 class="h2 pb-0">Full set list</h2>
<ol>
<li>Strange Frontier <small>(<em>Strange Frontier</em>, 1984)</small></li>
<li>Nation of Haircuts <small>(<em>Electric Fire</em>, 1998)</small></li>
<li>Gangsters Are Running This World [non-Purple version] <small>(<em>Outsider</em>, 2021)</small></li>
<li>Surrender <small>(<em>Electric Fire</em> <span class="post-italic-char">)</span></small></li>
<li>I See You Now <small>(<em>Violence Insane in a Beautiful World</em>, 2026)</small></li>
<li>I’m In Love With My Car <small>(Queen, <em>A Night At The Opera</em>, 1975)</small></li>
<li>Absolutely Anything <small>(<em>Outsider</em><span class="post-italic-char">)</span></small></li>
<li>Violence Insane <small>(<em>Violence Insane…</em>)</small></li>
<li>Don’t Photograph Food <small>(<em>Violence Insane…</em>)</small></li>
<li>These Are The Days Of Our Lives <small>(Queen, <em>Innuendo</em>, 1991)</small></li>
<li>Rock It (Prime Jive) <small>(Queen, <em>The Game</em>, 1980)</small></li>
<li>Jealous Guy <small>(John Lennon, <em>Imagine</em>, 1971)</small></li>
<li>Dear Mr Murdoch <small>(<em>Happiness?</em><span class="post-italic-char">,</span> 1994; 2011)</small></li>
<li>Chump <small>(<em>Violence Insane…</em>)</small></li>
<li>Under Pressure <small>(Queen, <em>Hot Space</em>, 1982)</small></li>
<li>Man on Fire <small>(<em>Strange Frontier</em><span class="post-italic-char">)</span></small></li>
<li>Rock and Roll <small>(Led Zeppelin, <em>Led Zeppelin IV</em>, 1971)</small></li>
<li>A Kind Of Magic <small>(Queen, <em>A Kind Of Magic</em>, 1986)</small></li>
<li><em>Encore:</em> A Beautiful World <small>(<em>Violence Insane…</em>)</small></li>
<li><em>Encore:</em> Come On Summer (It’s Party Time) <small>(<em>Violence Insane…</em>)</small></li>
<li><em>Encore:</em> Radio Ga Ga <small>(Queen, <em>The Works</em>, 1984)</small></li>
</ol>
<hr>
<h2 class="h5">Related posts</h2>
<ul class="footnotes mt-0">
  <li><a href="/posts/roger-taylor-ticket-tracker-2026/">Roger Taylor 2026 tour ticket tracker</a></li>
  <li><a href="/posts/freddie-mercury-lost-songs/">Hand-written lyrics for lost Freddie Mercury songs up for auction</a></li>
</ul>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Roger Taylor 2026 tour ticket tracker</title>
      <link href="https://www.jamieonkeys.dev/posts/roger-taylor-ticket-tracker-2026/"/>
      <updated>2026-09-22T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/roger-taylor-ticket-tracker-2026/</id>
      <content type="html">
        <![CDATA[
      <p><a href="https://www.patreon.com/wrestletix">WrestleTix</a> is a Twitter and Patreon account which tracks ticket sales for pro-wrestling events by monitoring mostly Ticketmaster seating maps. I thought I’d do the same for Queen drummer Roger Taylor’s 2026 tour of the UK, just to have the information out there for future historians. It’s a short, six-date tour to promote his latest solo album <em><a href="https://rogertaylor.lnk.to/VIIABW">Violence Insane in a Beautiful World</a></em>.</p>
<div class="table-container">
    <table>
        <thead>
            <tr>
                <th style="min-width: 100px;">Concert (2026)</th>
                <th>Seats (non-resale) available</th>
                <th>Available via resale</th>
                <th>Venue capacity as set up</th>
                <th>Tickets sold*</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <th>O2 City Hall Newcastle, 21 Sep</th>
                <td>321</td>
                <td>24</td>
                <td>2,121</td>
                <td><b>1,776</b> (<b>84.9%</b>)</td>
            </tr>
            <tr>
                <th>Usher Hall, Edinburgh, 22 Sep</th>
                <td>0</td>
                <td>13</td>
                <td>2,138</td>
                <td><b>2,138</b> (<b>100%</b>)</td>
            </tr>
            <tr>
                <th>The Alexandra, Birmingham, 24 Sep</th>
                <td>0</td>
                <td>4</td>
                <td>1,385</td>
                <td><b>1,385</b> (<b>100%</b>)</td>
            </tr>
            <tr>
                <th>Opera House, Manchester, 25 Sep</th>
                <td>0</td>
                <td>10</td>
                <td>1,915</td>
                <td><b>1,915</b> (<b>100%</b>)</td>
            </tr>
            <tr>
                <th>Roundhouse, London, 29 Sep**</th>
                <td>?</td>
                <td>?</td>
                <td>1,800?, of which 514 are seated</td>
                <td>514 seated + ? standing</td>
            </tr>
            <tr>
                <th>Swansea Building Society Arena, Swansea, 29 Sep</th>
                <td>0</td>
                <td>30</td>
                <td>2,579</td>
                <td><b>2,579</b> (<b>100%</b>)</td>
            </tr>
        </tbody>
        <tfoot>
            <tr>
                <td colspan="5">Last updated: 29 Sep 2026 at 22:20</td>
            </tr>
        </tfoot>
    </table>
</div>
<p>* I continued counting until an hour or so before each gig started and the ticket map went offline. Resales tickets count as sold.<br>
** All 514 seated tickets (Level 2) were sold for the London date. Standing area tickets are not countable on the Ticketmaster website. As of 22 September, you could only buy resale tickets, which indicated all floor tickets had been sold. However, on the 25th, standard (non-resale) tickets were available again. It seems venue capacity is in the region of <a href="https://www.roundhouse.org.uk/venue-hire/standing-receptions/#:~:text=Our%20iconic%20Main,of%20a%20party.">1,800</a>. (Gigs with Ivan has a <a href="https://gigswithivan.uk/2024/04/23/the-london-roundhouse/">write-up</a> of the venue in concert setup. He mentions <a href="https://gigswithivan.uk/2024/04/23/the-london-roundhouse/#:~:text=The%20capacity%20is%20still%20a%20bit%20unclear%2E%20I%20think%20it%E2%80%99s%201800%20for%20standing%20gigs%2C%20which%20includes%20the%20fairly%20small%20circular%20balcony">gig capacity being unclear</a>, but thinks it’s 1,800.).</p>
<p>Other note: all VIP tickets were sold in all venues. Some may be available via resale – it’s not something I tracked.</p>
<h2 class="h2">Filming</h2>
<p>It looks like the London concert was filmed. Videographers <a href="https://www.creokinetics.com/">CreoKinetics</a> <a href="https://www.instagram.com/p/Dd6CSvUjKcH/?stkn=MTYyNDN0M3hhMGllaA%3D%3D&amp;img_index=2">posted</a> on Instagram on 30 September that they’d been ‘providing and operating 5x fixed rig cameras and 1x 4K <a href="https://en.wikipedia.org/wiki/Pan%E2%80%93tilt%E2%80%93zoom_camera">PTZ</a> onstage […], alongside a crew of front of house cameras.’ We got a live album (<a href="https://www.udiscovermusic.com/news/roger-taylor-outsider-tour-live-out-now/"><em>The Outsider Tour Live</em></a>, 2022) out of the previous tour. Some kind of live audio-visual release for the <em>Violence Insane…</em> era would be just braw!</p>
<hr>
<p class="footnotes">Related post: <a href="/posts/roger-taylor-usher-hall/">Roger Taylor rocks the Usher Hall</a>.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Hand-written lyrics for lost Freddie Mercury songs up for auction</title>
      <link href="https://www.jamieonkeys.dev/posts/freddie-mercury-lost-songs/"/>
      <updated>2026-09-05T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/freddie-mercury-lost-songs/</id>
      <content type="html">
        <![CDATA[
      <p>An <a href="https://propstoreauction.com/search?key=Reinhold+Mack+Collection&amp;xclosed=0">auction</a> of Freddie Mercury-related items owned by Reinhold Mack, who co-produced Freddie’s 1985 album <em>Mr Bad Guy</em>, is currently under way. There are a decent number of lots containing hand-written lyrics, some for apparently lost songs, including two notebooks with starting prices of <a href="https://propstoreauction.com/lot-details/index/catalog/522/lot/212539/Lot-264-The-Reinhold-Mack-Collection-Notebook-Containing-Collection-of-Freddie-Mercury-Handwritten-Working-Lyrics-and-Notes-from">£7,500</a> and <a href="https://propstoreauction.com/lot-details/index/catalog/522/lot/212544/Lot-269-The-Reinhold-Mack-Collection-Notebook-Containing-Freddie-Mercury-Handwritten-Work-In-Progress-and-Unreleased-Songs-FREDD">£5,000</a>.</p>
<figure class="w-950">
  <img src="/img/freddie-solo-collage.jpg?v=3" width="950" height="1267" alt="A collage of hand-written lyrics and other documents from an auction of materials related to Freddie Mercury’s 1985 album ‘Mr Bad Guy’. Some lyrics appear to be for previously unknown songs.">
  <figcaption>A collage of hand-written lyrics and other documents from an auction of materials relating to the recording sessions for Freddie Mercury’s 1985 album <em>Mr Bad Guy</em>. Some lyrics appear to be for songs hitherto unknown to fans, such as ‘Tripping Love’ and ‘I Can’t Sleep at Nights’. (Source images: Propstore Auction)</figcaption>
</figure>
<h2 id="lyrics-to-unknown-songs">Lyrics to unknown songs</h2>
<p>There are plenty of lyrics that I can’t attribute to existing songs, including a few with a working title at the top of the page. Such named songs include ‘Tripping Love’, ‘I Can’t Sleep at Nights’, ‘Don’t Give Up’ and ‘The River of Lasting Love[?s]’. These could in theory be early versions of other songs. At first glance, it’s tempting to flag ‘Tripping Love’ as an early version of ‘Mr Bad Guy’, as it includes the line ‘I’m walking rainbows in the sky’, similar to ‘Let’s go chasing rainbows in the sky’ from the album’s title track; and the lyrics on the opposing page certainly <em>are</em> for an early or alternative version of ‘Mr Bad Guy’. However, ‘Tripping Love’, underlined at the top of the page, is clearly marked as the song’s title, and the lyrics don’t scan with ‘Mr Bad Guy’ if you sing the familiar melody in your head. Perhaps it’s a track being written in parallel with ‘Mr Bad Guy’ and the lines about rainbows were being tried out in both.</p>
<p><a href="https://en.wikipedia.org/wiki/The_Solo_Collection"><em>The Solo Collection</em></a> (2000) was about as comprehensive as it was possible to be in terms of presenting available Freddie-related outtakes and alternative versions, so we have to assume no demos of these mystery tracks are in the official archive. Could some demos exist as home recordings? Mary Austin, Freddie’s longtime close personal friend to whom he left <a href="https://www.theguardian.com/music/2026/sep/01/freddie-mercury-lover-mary-austin-interview#:~:text=As%20does%20half%20of%20his%20estate">half of his estate</a>, notes in her recent book <em><a href="https://www.alifeinlyrics.com">Freddie Mercury: A Life in Lyrics</a></em> (2026, HarperCollins) that a number of cassette tapes, ‘the kind Freddie had used to record himself when he was writing’, were found in his piano stool in 2023 while readying his possessions for auction<sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup>. No information is given as to what, if anything, is on these cassettes. (There does exist a <a href="https://www.youtube.com/watch?v=E2GV7ULYZnE">leaked home demo</a> of unreleased <em>Mr Bad Guy</em> track ‘New York’).</p>
<p>When composing a song, Freddie tended to come up with the music first before focusing on the lyrics later<sup class="footnote-ref"><a href="#fn2" id="fnref2">2</a></sup>, so I have to imagine melodies and chords would have existed for these song drafts, even if only in Freddie’s head. It goes without saying that it would be nice to hear any demo recordings he made of them.</p>
<h2 id="interesting-track-listings">Interesting track listings</h2>
<figure class="w-950" style="margin-top: 1.6rem; margin-bottom: 1.4rem;">
  <img class="nae-shadow" src="/img/freddie-lyrics.png" width="950" height="675" alt="Two listings of tracks worked on during the sessions for Freddie Mercury’s 1985 album ‘Mr Bad Guy’.">
  <figcaption style="padding-top:0; margin-top: -6px;">A couple of track listings for <em>Mr Bad Guy</em>. The one on the right resembles an album running order, and has newer track names (‘Let’s Turn It On’ as opposed to ‘Let’s Get It On’, and ‘Fooling Around’ instead of ‘Fooling Around With Me’). (Source images: Propstore Auction)</figcaption>
</figure>
<p>Also in amongst the items in the collection are a couple of track listings. One (on the right in the image above) is shorter and divided in the middle with a row of ‘X’s, suggesting a two-sided album running order. Besides tracks which ended up on the album or as B-sides, there are demos which appeared in the Freddie <em>Solo Collection</em> box: ‘Money Can’t Buy Happiness’, ‘Gazelle’ and ‘God is Heavy’. ‘You Are The Only One’ is noted in <a href="https://www.amazon.co.uk/Queen-Complete-Works-revised-updated/dp/1789090008/ref=sr_1_2"><em>Queen: Complete Works</em></a> (Georg Purvis, updated edition, 2018, Titan Books) as being from the <em>Kind of Magic</em> sessions (1985–6)<sup class="footnote-ref"><a href="#fn3" id="fnref3">3</a></sup>. I guess we can now say the song started life a bit earlier. ‘Whipping Boy’ may be a Queen track started during the <em>Hot Space</em> sessions in 1981–2 (Purvis). ‘Piece of My Heart’ will be what became ‘Let Me Live’ on Queen’s <em>Made In Heaven</em> (released 1995). ‘Killing Time’ may be a Freddie version of Queen drummer Roger Taylor’s solo track of the same name from <em>Strange Frontier</em> (1984). I have nae idea about ‘Press On (Presto)’, ‘New Piano Song’, ‘One Night Stand’, ‘A Desperate Man’, ‘Make it On My Own’ and ‘Flesh is on the Bone’.</p>
<p>‘Killing Time’ is interesting: it appears in both lists, with a tick next to it in the first, and a question mark in the second<sup class="footnote-ref"><a href="#fn4" id="fnref4">4</a></sup>. Judging from the track names, the second listing is from a later date, suggesting the track was in the running for inclusion on the album for at least some period of time. Does this mean a complete Freddie solo version exists? I don’t know, but it would be amazing to hear. Freddie did provide backing vocals on Roger’s version (Purvis) – check out the bit from 3:11. If this song was also worked on by Queen during sessions for <em>The Works</em>, it could well turn up on any future <em>Works</em> box set. There was a lot of cross-pollination between Freddie, Roger and Queen at the time, and any interesting outtakes or alternative versions would likely be reserved for a Queen archival project<sup class="footnote-ref"><a href="#fn5" id="fnref5">5</a></sup>.</p>
<h2 id="who-plays-what-on-mr-bad-guy">Who plays what on <em>Mr Bad Guy</em></h2>
<figure class="w-950" style="margin-top: 1.6rem; margin-bottom: 1.4rem;">
  <img class="nae-shadow" src="/img/mr-bad-guy-musicians.png" width="950" height="617" alt="Three sheets of paper detailing who played what instruments on Freddie Mercury’s ‘Mr Bad Guy’ album.">
  <figcaption style="margin-top: -3px;">Details of who played what instruments on <em>Mr Bad Guy</em>. (Source images: Propstore Auction)</figcaption>
</figure>
<p>Finally, there is a listing of who plays what on the <em>Mr Bad Guy</em> tracks, giving us much more information than the album liner notes do. The three-page document, entitled ‘Musicians’ and dated 14 February 1985 (ten weeks before the album’s release on <a href="https://en.wikipedia.org/wiki/Mr._Bad_Guy#:~:text=29%20April%201985">29 April</a>), describes who plays what instruments and on which tracks<sup class="footnote-ref"><a href="#fn6" id="fnref6">6</a></sup>. Roger Taylor is down as having played drums on ‘Man Made Paradise’, ‘Made in Heaven’ and ‘There Must Be More To Life Than This’, as well as drum fills on ‘Let’s Turn It On’. I don’t believe this has been confirmed before now (Roger mentions in the <a href="https://freddiemercury.com/documentary/the-great-pretender-documentary/"><em>The Great Pretender</em></a> documentary (2012) that he played on the album, without specifying in what capacity, only mentioning Freddie asking him to come to Munich to add some backing vocals). The drummer credited in the liner notes, Curt Cress, is listed under ‘Love Me Like There’s No Tomorrow’, ‘Mr Bad Guy’, ‘She Blows Hot and Cold’ and ‘My Love is Dangerous’, as well as for drum fills on ‘Your Kind of Lover’. It’s of course entirely possible that, like Brian May’s contribution to ‘She Blows Hot and Cold’, Roger’s parts were subsequently replaced by the musician who appears in the official credits<sup class="footnote-ref"><a href="#fn7" id="fnref7">7</a></sup>.</p>
<h2 id="random-tidbits">Random tidbits</h2>
<ul>
<li>A random factoid known to the more dedicated of fans is that Freddie was approached to audition for a part in Puccini’s opera <em>La bohème</em> opposite Linda Ronstadt. The offer letter is in the auction and dated 13 July 1984. It notes, in a rather condescending manner: ‘The role of Rodolfo is very difficult vocally. I have enclosed a selection of the score that we will be hearing for the audition. If Mr. Mercury is interested we would like him to sing for the director in early August. Please let us know as soon as possible.’ There are a few notes hand-written on the score in all caps, the tone of which is quite arsey: ‘Sing only these lyrics, not others’; ‘1. Do not sing in another key’; ‘2. Sing high notes in full voice, not falsetto’ (with the ‘not’ double-underlined). Freddie was taken aback at being approached, and wasn’t interested, finding the prospect of singing opera for six or seven nights a week unappealing (‘I’d be bored to death’)<sup class="footnote-ref"><a href="#fn8" id="fnref8">8</a></sup>.</li>
<li>A printed draft of album credits ends ‘This album is dedicated to all cat lovers[.] Fuck the rest of you cunts..Freddie Mercury’. The eventual dedication was the slightly tamer ‘This album is dedicated to my cat Jerry – also Tom, Oscar and Tiffany, and all the cat lovers across the universe – screw everybody else.’ Another draft has ‘fuck everybody else’ (and can be seen in the collage at the top of this post).</li>
</ul>
<p>⁂</p>
<p>All said, for a Queen and Freddie geek like myself, this is an interesting cache of documents! Don’t think I can afford to bid on any of them, unfortunately.</p>
<hr>
<h2 class="h5">Related posts</h2>
<ul class="footnotes mt-0">
  <li><a href="/posts/roger-taylor-usher-hall/">Roger Taylor rocks the Usher Hall</a></li>
  <li><a href="/posts/roger-taylor-ticket-tracker-2026/">Roger Taylor 2026 tour ticket tracker</a></li>
</ul>
<hr class="footnotes-sep">
<section class="footnotes">
<h2 class="h5">Footnotes</h2>
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>I don’t believe they were included in the 2023 Sotheby’s auction (the Sotheby’s website returns nothing if you <a href="https://www.sothebys.com/en/buy/auction/2023/freddie-mercury-a-world-of-his-own-the-evening-sale?locale=en&amp;lotFilter=AllLots&amp;search=cassette">search</a> <a href="https://www.sothebys.com/en/buy/auction/2023/freddie-mercury-a-world-of-his-own-on-stage?locale=en&amp;lotFilter=AllLots&amp;search=cassette">the</a> <a href="https://www.sothebys.com/en/buy/auction/2023/freddie-mercury-a-world-of-his-own-at-home?locale=en&amp;lotFilter=AllLots&amp;search=cassette">Freddie</a> <a href="https://www.sothebys.com/en/buy/auction/2023/freddie-mercury-a-world-of-his-own-in-love-with-japan?locale=en&amp;lotFilter=AllLots&amp;search=cassette">auction</a> results for ‘cassette’). <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>To quote from <em>Freddie Mercury: a life, in his own words</em> (a quasi-autobiography compiled and edited by Greg Brooks and Simon Lupton, second edition, 2019): ‘My strongest point is melody content. I concentrate on that first, then the song structure, then the words come after. The structure of the melody comes easy to me, it’s the lyrical content that I find difficult. I have to work on that part of it. I sometimes feel that my melodies are so much stronger than my lyrics, that the lyrics bring them down. I hate writing lyrics. I wish somebody else could do it. I wish I had a Bernie Taupin. Mind you, I like to do it all myself. I’m a greedy bitch.’ (Part of this quote is from a <a href="https://www.youtube.com/watch?v=IGZaPM2JR_E&amp;t=36s">1985 interview</a> with David Wigg.) An example of his typical way of working can be heard in <a href="https://www.youtube.com/watch?v=QUFGQqSW9QU">this early home demo</a> of the Queen song ‘Keep Passing the Open Windows’, where he plays an arrangement alone on the piano, while vocalising the melody. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>A recording <a href="https://www.youtube.com/watch?v=sCbsHjH4cHQ">leaked online</a> some years ago. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>Other tracks have a ‘D’ next to them. Does this perhaps mean ‘Done’? <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p>As an example, on the Freddie <em>Solo Collection</em> box there were no ‘Man Made Paradise’ outtakes. The song started as a Queen track during the <a href="https://www.udiscovermusic.com/stories/freddie-mercury-mr-bad-guy-solo-album/#:~:text=The%20track%20%E2%80%9CMan%20Made%20Paradise%E2%80%9D%20had%20originally%20been%20considered%20for%20the%201981%20Queen%20album%20Hot%20Space">Hot Space sessions</a>, and was later considered for <em>The Works</em> (released 1984) (Purvis). I have always assumed its origin as a Queen track was the reason for its exclusion from the <em>Solo Collection</em> CDs. (An unidentified version of the song appears in the soundtrack of <em>Solo Collection</em> documentary <a href="https://freddiemercury.com/documentary/the-untold-story/"><em>The Untold Story</em></a>, and it’s possible this is a snippet from a Queen version.) Similarly demonstrating that Queen takes precedence, the boxset contains no demos of ‘Africa by Night’, which <a href="https://www.youtube.com/watch?v=aj146IA-PlU">started as Freddie solo song</a> post-<em>Mr Bad Guy</em> but ended up as ‘All God’s People’ on <em>Innuendo</em> (released 1991). <a href="#fnref5" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn6" class="footnote-item"><p>The named musicians, in order of appearance in the document, are: Reinhold Mack on <a href="https://en.wikipedia.org/wiki/LinnDrum">LinnDrum</a>; Stephan Wissnet on bass, guitar, <a href="https://en.wikipedia.org/wiki/Fairlight_CMI">Fairlight</a>, <a href="https://en.wikipedia.org/wiki/Kurzweil_Music_Systems">Kurzweil</a> and ‘Sequ.’ (sequenced?) bass; Freddie on piano, Kurzweil, ‘trig bass’ and <a href="https://en.wikipedia.org/wiki/Roland_Jupiter-8">Jupiter-8</a>; Curt Cress and Roger Taylor on drums; <a href="https://www.udiscovermusic.com/stories/freddie-mercury-mr-bad-guy-solo-album/#:~:text=fretless%20bass%20from%20guest%20musician%20Jo%20Burt">Jo Burt on fretless bass</a>; Paul Vincent and Brian May on guitar (Brian on ‘She Blows Hot and Cold’ only; replaced by Paul Vincent on the released album); Fred Mandel on piano, Jupiter-8 and guitar; and the Munich Symphonic Orchestra conducted by Rainer Pietsch. <a href="#fnref6" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn7" class="footnote-item"><p>The drumming on the three tracks Roger is noted as playing on certainly <em>sounds</em> like Roger (especially on ‘Man Made Paradise’ and ‘There Must Be More To Life Than This’, where you can hear his <a href="https://www.youtube.com/watch?v=vxIM5b6CRM4&amp;t=145s">trademark technique</a> of briefly opening the hi-hat as he hits the snare on the backbeat), but it may have been a case of Freddie directing Curt Cress to play in Roger’s style, just as he got Paul Vincent to play like Brian May on ‘She Blows Hot and Cold’ (according to the notes that accompanied <em>The Solo Collection</em>). <a href="#fnref7" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn8" class="footnote-item"><p><em>Freddie Mercury: a life, in his own words</em> (Brooks and Lupton). <a href="#fnref8" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Fix for missing ‘AScore’ font when editing Logic Pro PDFs</title>
      <link href="https://www.jamieonkeys.dev/posts/logic-ascore-font/"/>
      <updated>2026-08-12T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/logic-ascore-font/</id>
      <content type="html">
        <![CDATA[
      <blockquote>
<p>tl;dr: I found the AScore font in <code>/Applications/Logic Pro.app/Contents/Frameworks/MACore.framework/Versions/A/Resources</code>, <a href="#heading-installing-the-fonts">installed</a> it with <a href="https://support.apple.com/en-gb/guide/font-book/welcome/mac">Font Book</a>, and can now open Logic’s PDFs in Affinity without rendering problems.</p>
</blockquote>
<p>I use Apple’s <a href="https://www.apple.com/logic-pro/">Logic Pro</a> to produce <a href="https://jamiealexandersmith.com/#charts">written musical scores</a>. Logic is a bit of an eccentric choice for this purpose: its scoring functionality has its share of quirks, and probably doesn’t quite match a dedicated scoring application like Dorico in terms of power and flexibility. But it gets the job done and is actually <a href="https://macprovideo.com/course/logic-pro-x-109-core-training-the-score-editor">quite capable</a>.</p>
<p>Often I want to open a PDF of a score exported from Logic in an application like Affinity or Adobe Illustrator for light editing. Logic uses internal fonts called ’AScore‘ and ’AScoreParts’ for musical glyphs (note heads, time signatures etc.), which don’t render when a PDF produced by Logic is opened in a vector editing tool (you get a ‘missing font’ message).</p>
<p>The problem is most noticeable when using Internal as the <a href="https://support.apple.com/en-gb/guide/logicpro/lgcp85357732/mac">symbol font</a> (see screenshot below). However, even when using a third-party font like <a href="https://jazzfont.com">Jazz</a> or <a href="https://github.com/steinbergmedia/petaluma">Petaluma</a> (my preferred hand-written font; see later screenshot), PDFs will fall back to using the Internal Font (AScore and AScoreParts) when a glyph/symbol is unavailable in the third-party font, or is perhaps mapped differently.</p>
<figure class="w-950">
  <img src="/img/logic-score-internal-font-missing.jpg" width="950" height="311" alt="Part of the ‘Lang-Leggit Lurcher’ score exported from Logic as a PDF then opened in Affinity, showing numerous missing glyphs due to the missing AScore font.">
  <figcaption><strong>What the glyph?</strong> Part of the score for my tune <a href="https://jamiealexandersmith.com/#charts">‘The Lang-Leggit Lurcher’</a> exported from Logic as a PDF (using Internal as the <a href="https://support.apple.com/en-gb/guide/logicpro/lgcp85357732/mac">symbol font</a>) then opened in Affinity, without Logic’s internal AScore installed as a standard system font.</figcaption>
</figure>
<p>This problem <a href="https://dsl.ac.uk/entry/snd/bumbaze">bumbazed</a> me for the longest time, but I finally figured out a fix.</p>
<p>AScore and AScoreParts aren’t installed along with the other system fonts, but I knew they had to be somewhere in the file system. Maybe I could find them and install them along with the others, which would allow them to be picked up by other applications. To my delight, this theory panned out. I located the fonts, installed them, and I can now open Logic PDFs in Affinity for editing (which is sometimes necessary when I can’t quite get Logic to format exactly the way I want it to – as I said, the scoring functionality can be quirky).</p>
<figure class="w-950">
  <img src="/img/lll-before-after-ascore.png" class="nae-shadow img-transparent bg-rainbow-avatar animate" width="950" height="670" alt="Side-by-side comparison of my score for ‘The Lang-Leggit Lurcher’, with rendering issues (on the left; problematic glyphs highlighted in yellow), and rendered properly (on the right) after installing the AScore font on my Mac.">
  <figcaption>Before installing the AScore font, my score for ‘The Lang-Leggit Lurcher’ had rendering issues (left, highlighted in yellow) when opening in Affinity. After locating and installing the font, it opens as expected (right).</figcaption>
</figure>
<h2 id="installing-the-fonts">Installing the fonts</h2>
<p>On my machine (running Logic Pro 12.3 on macOS Tahoe 26.6.1), AScore and AScoreParts are stored in <code>/Applications/Logic Pro.app/Contents/Frameworks/MACore.framework/Versions/A/Resources</code>. To install them, I opened Finder and selected ‘Go to Folder…’ from the ‘Go’ menu, pasted in the aforementioned path (adjust it if your Logic app lives elsewhere, such as in the <code>Users</code> folder), and hit <kbd>Enter</kbd>. I then saw <code>AScore.ttf</code> and <code>AScoreParts.ttf</code> listed. Woop. I double-clicked both to open and install them via Font Book. They’re then picked up when I open my PDF scores in Affinity. (I’ve only actually encountered AScore in the wild, but I installed AScoreParts too in case I use one of its symbols in the future.)</p>
<p>The Logic folder structure may well change with a subsequent version. If you’re reading this and the fonts are no longer at the path where I found them, try searching via Terminal:</p>
<ol>
<li><code>cd '/Applications/Logic Pro.app'</code></li>
<li><code>find . -name &quot;*AScore*&quot;</code></li>
</ol>
<p>(I was originally going to post all this as a reply to <a href="https://forum.affinity.serif.com/index.php?/topic/24911-music-fonts-and-pdf-files/">a thread</a> on the Affinity forums, but I see they’re now read-only, <a href="https://forum.affinity.serif.com/index.php?/topic/235708-important-update-affinity-forum-transition/">replaced</a> with a Discord community. That’s not a lot of use for people searching for answers. Apparently, moving communities to Discord is a <a href="https://kotaku.com/discord-forum-messageboard-amazon-lost-ark-new-world-pc-1850393695">trend</a>.)</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Water consumption: AI vs animal agriculture</title>
      <link href="https://www.jamieonkeys.dev/posts/ai-animal-agriculture-water/"/>
      <updated>2026-06-28T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/ai-animal-agriculture-water/</id>
      <content type="html">
        <![CDATA[
      <p><em>There has been much gnashing of teeth online over AI data centres and their water consumption. But just how much water does AI actually use, and how does this compare to animal farming? I looked into the numbers…</em></p>
<p><a href="https://arxiv.org/abs/2304.03271">One study</a> predicts that AI water consumption globally will be between <a href="https://arxiv.org/html/2304.03271v5#:~:text=a%20total%20of%200.38%20%E2%80%93%200.60%20billion%20cubic%20meters%20of%20water%20will%20be%20evaporated,%20in%202027">0.38 and 0.60 km³ in 2027</a>. Animal agriculture, by comparison, is much, much worse, consuming <a href="https://www.mdpi.com/2073-4441/16/12/1681#:~:text=Globally%2C%20228%20km3%20of%20water%20is%20consumed%20by%20livestock%20agri%2Dfood%20systems">228 km³</a> <a href="https://www.mdpi.com/2073-4441/16/12/1681#:~:text=all%20values%20reported%20here%20are%20annual%20averages">annually</a>. In other words, <span class="highlight-text"><strong>exploiting animals for their flesh, milk and eggs consumes roughly 380× to 600× as much water as AI does</strong></span>. To put the volumes in perspective: in 2027 AI is predicted to consume between a twentieth and a twelfth of the volume of <a href="https://www.nature.scot/landscapes-and-habitats/habitat-types/lochs-rivers-and-wetlands/freshwater-lochs#:~:text=7%2C452%20million%20cubic%20metres">water in Loch Ness</a> (Scotland’s largest loch by volume, and the <a href="https://web.archive.org/web/20160303211946/http://www.snh.org.uk/publications/on-line/advisorynotes/4/4.htm#:~:text=In%20terms%20of%20volume,%20England%20and%20Wales%20combined">largest</a> body of water by volume in Britain), whereas animal farming already consumes just over <em>30 entire Loch Nesses</em> annually!</p>
<p>(‘Consumption’ here means water which is evaporated and lost, as opposed to ‘withdrawal’, which is largely returned to the system. Water withdrawal for AI in 2027 is predicted to be between <a href="https://arxiv.org/html/2304.03271v5#:~:text=the%20global%20AI%20demand%20is%20projected%20to%20account,%20withdrawal%20in%202027">4.2 and 6.6 km³</a>. Animal agriculture, in contrast, withdraws <a href="https://www.mdpi.com/2073-4441/16/12/1681#:~:text=Out%20of%20the%203670,13%20km3">566 km³ per year</a>: 513 km³ for feed, 40 km³ for drinking water and 13 km³ for ‘service’ [<a href="https://wisy-water.com/en/service-water-definition-and-benefits/#:~:text=Service%20water%20is%20water%C2%A0%20used%20in%20private%2C%20technical%2C%20commercial%20and%20agricultural%20activities%20for%20needs%20that%20do%20not%20require%20potable%20water%20quality">non-potable</a>] water.)</p>
<figure class="w-950">
  <img src="/img/loch-ness.jpg" width="950" height="442" alt="Photograph of Loch Ness.">
  <figcaption>In 2027, AI is set to consume at most a twelfth (less than 10 per cent) of the volume of Loch Ness, Scotland’s <a href="https://dsl.ac.uk/entry/snd/muckle">mucklest</a> loch. Conversely, animal farming consumes annually the equivalent of the entirety of Loch Ness <em>thirty times over</em>. Photo by <a href="https://unsplash.com/@laurenholding">Lauren Holding</a> on <a href="https://unsplash.com/photos/a-body-of-water-with-mountains-in-the-background-GqVd1rwAlos">Unsplash</a>.</figcaption>
</figure>
<p>This is not to say there isn’t a problem with water usage by AI, especially in localities hosting data centres. And there are of course other environmental impacts of both <a href="https://en.wikipedia.org/wiki/Environmental_impact_of_AI">AI</a> and <a href="https://en.wikipedia.org/wiki/Environmental_impacts_of_animal_agriculture">animal agriculture</a>, not to mention the <a href="https://animalequality.org.uk/investigations/">serious ethical violations</a> <a href="https://viva.org.uk/animals/">inherent</a> in the latter.</p>
<p>However, if water conservation and the environment in general concern you, you’re much better off adopting a <a href="https://viva.org.uk/planet/">plant-based diet</a> (and advocating for it to others, including <a href="https://plantbaseduniversities.org">institutions</a> and <a href="https://plantbasedtreaty.org">governments</a>) than worrying too much about AI.</p>
<hr>
<p class="footnotes">Featured post: <a href="/posts/freddie-mercury-lost-songs/">Hand-written lyrics for lost Freddie Mercury songs up for auction</a>.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Building an RSS news aggregator with Drupal</title>
      <link href="https://www.jamieonkeys.dev/posts/news-aggregator-drupal/"/>
      <updated>2024-12-10T00:00:00+00:00</updated>
      <id>https://www.jamieonkeys.dev/posts/news-aggregator-drupal/</id>
      <content type="html">
        <![CDATA[
      <p><a href="https://animalrights.fyi">AnimalRights.fyi</a> is a news aggregator I built to pull together RSS feeds from animal rights and vegan news sources into a single location. The goal is to make this information easily accessible while linking to the people and organisations working to reduce animal suffering.</p>
<p>This blog post write-up is both for my own future reference and for anyone else who might find it helpful.</p>
<figure class="w-950">
  <video class="br-10" poster="/img/arfyi-31-poster.png" autoplay playsinline muted loop width="950" height="724"><source src="/img/arfyi-31.mp4" type="video/mp4">Your browser does not support the video tag.</video>
  <figcaption>
    AnimalRights.fyi in action, showing how the feed can be filtered by headline, and how users can react to items with the emoji icons.
  </figcaption>
</figure>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#heading-introduction">Introduction</a></li>
<li><a href="#heading-adding-functionality-with-custom-modules">Adding functionality with custom modules</a></li>
<li><a href="#heading-the-view">The View</a>
<ul>
<li><a href="#heading-outputting-the-html-via-the-fields-section">Outputting the HTML via the Fields section</a></li>
<li><a href="#heading-best-practice">Best practice</a></li>
<li><a href="#heading-filtering-the-view-output">Filtering the View output</a></li>
<li><a href="#heading-accessing-feed-custom-fields-with-relationships">Accessing feed custom fields with Relationships</a></li>
<li><a href="#heading-add-some-ajax">Add some AJAX</a></li>
</ul>
</li>
<li><a href="#heading-refreshing-the-feeds">Refreshing the feeds</a></li>
<li><a href="#heading-conclusion">Conclusion</a></li>
</ul>
<h2 id="introduction">Introduction</h2>
<p>The core functionality is based on a <a href="https://drupalize.me/tutorial/overview-views-api-drupal">View</a> and the <a href="https://www.drupal.org/project/aggregator">Aggregator module</a>. The site also uses the <a href="https://www.drupal.org/project/rate">Rate</a> and <a href="https://www.drupal.org/project/votingapi">Voting API</a> modules to enable users to ‘react’ to any news item by tapping on one of the emojis. The theme is a subtheme of Olivero, which ships with Drupal core. I quickly set up Drupal using SiteGround’s <a href="https://www.siteground.co.uk/tutorials/drupal/app-installer-installation">app installer</a>. (Handily, the install comes with <a href="https://drupalize.me/tutorial/what-composer">Composer</a>, <a href="https://drupalize.me/course/learn-drush-drupal-shell">Drush</a> and git out of the box.)</p>
<h2 id="adding-functionality-with-custom-modules">Adding functionality with custom modules</h2>
<p>As well as <a href="https://drupalize.me/tutorial/concept-using-and-improving-contributed-modules">contributed modules</a> Aggregator and Rate, there are three custom modules:</p>
<h3 id="%E2%80%98cookie-voter%E2%80%99">‘Cookie Voter’</h3>
<p><a href="https://github.com/donbrae/cookie_voter">Cookie Voter</a> alters the Rate module so that it uses cookies instead of IP addresses to track anonymous emoji reactions. Using IP addresses can cause users on shared networks to see others’ reactions appear as their own, which is confusing. BuzzFeed takes a similar approach, storing user reactions in the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage">browser’s <code>localStorage</code></a>. On the other hand, the <a href="https://bearblog.dev/discover/">Bear blogging platform</a> appears to use IP addresses to record upvotes. In Bear’s case that’s a sensible approach as votes affect a list of trending posts. At AnimalRights.fyi, emoji reactions simply offer users an informal way to engage with news stories.</p>
<h3 id="%E2%80%98custom-headline-filter%E2%80%99">‘Custom Headline Filter’</h3>
<p><a href="https://github.com/donbrae/custom_headline_filter">Custom Headline Filter</a> provides a <a href="https://drupalize.me/tutorial/overview-filter-criteria-views">Views filter</a> for removing duplicate (or overly similar) headlines. Such headlines can appear due to malformation of the incoming RSS feed, crossposting between websites, or when multiple news outlets report on the same story. There is a similarity threshold which can be adjusted to taste in the Views UI. The similarity is calculated by PHP’s <a href="https://www.php.net/manual/en/function.similar-text.php"><code>similar_text</code></a> function.</p>
<h3 id="%E2%80%98custom-twig-extensions%E2%80%99">‘Custom Twig Extensions’</h3>
<p>I needed to use PHP’s <code>preg_replace</code> function, and wanted to avoid installing the <a href="https://www.drupal.org/project/twig_extensions">Twig Extensions</a> module; so I created <a href="https://github.com/donbrae/custom_twig_extensions">Custom Twig Extensions</a>. (I try to install as few modules as possible to keep the complexity at a minimum and make site maintenance easier.)</p>
<h3 id="custom-modules-summary">Custom modules summary</h3>
<p>All three modules were basically written by Claude (3.5 Sonnet), though there was a fair amount of back and forth between it and myself. In the case of Cookie Voter, for example, I asked it to refactor <a href="https://www.drupal.org/project/rate/issues/1903262#comment-7170368">this Drupal 7 code</a> for Drupal 8+, but it took much prompting to achieve the desired result. ‘We’ eventually landed on a solution after I fed the AI some relevant code from the Rate module codebase. This is a useful tip I will bear in mind when problem solving with a chatbot in future: don’t assume it already ‘knows’ a given codebase. (I had a <a href="https://chatgpt.com/share/673f5109-29c0-800c-9416-606d51a2afb7">conversation</a> with ChatGPT about why supplying codebase excerpts for more obscure coding problems might be necessary.)</p>
<h2 id="the-view">The View</h2>
<p>Here’s a screenshot of the View, which is where the user-facing page is built:</p>
<figure class="w-950">
  <img src="/img/arfyi-view.png" width="950" height="1097" alt="A screenshot of the View which creates the listing of aggregated news items.">
  <figcaption>A screenshot of the View which creates the listing of aggregated news items. The output is formed in the Fields section in the left-most column.</figcaption>
</figure>
<h3 id="outputting-the-html-via-the-fields-section">Outputting the HTML via the Fields section</h3>
<p>The Fields section forms the HTML output of the View, with Twig as the templating language. Here’s the Fields section aggregated into a single template representation:</p>
<pre class="language-twig"><code class="language-twig"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>news-item <span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> title_1 <span class="token delimiter punctuation">}}</span></span> fade-in-quick<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h3</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>headline<span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">if</span> field_podcast <span class="token operator">==</span> <span class="token string"><span class="token punctuation">'</span>1<span class="token punctuation">'</span></span> <span class="token delimiter punctuation">%}</span></span> icon-podcast<span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">endif</span> <span class="token delimiter punctuation">%}</span></span><span class="token punctuation">"</span></span> <span class="token attr-name">iid</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> iid <span class="token delimiter punctuation">}}</span></span><span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> link <span class="token delimiter punctuation">}}</span></span><span class="token punctuation">"</span></span> <span class="token attr-name">target</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>_blank<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> title <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h3</span><span class="token punctuation">></span></span><br><br>  <span class="token twig language-twig"><span class="token comment">{# Get first paragraph of description and remove HTML tags #}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">set</span> first_paragraph <span class="token operator">=</span> description<span class="token operator">|</span>split<span class="token punctuation">(</span><span class="token string"><span class="token punctuation">'</span>&lt;/p><span class="token punctuation">'</span></span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token operator">|</span>trim <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">set</span> first_paragraph <span class="token operator">=</span> first_paragraph <span class="token operator">starts with</span> <span class="token string"><span class="token punctuation">'</span>&lt;p><span class="token punctuation">'</span></span> <span class="token operator">?</span> first_paragraph<span class="token punctuation">[</span><span class="token number">3</span><span class="token punctuation">:</span><span class="token punctuation">]</span> <span class="token punctuation">:</span> first_paragraph <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">set</span> first_paragraph <span class="token operator">=</span> first_paragraph<span class="token operator">|</span>striptags<span class="token operator">|</span>trim <span class="token delimiter punctuation">%}</span></span><br><br>  <span class="token twig language-twig"><span class="token comment">{# Handle truncation #}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">set</span> last_char <span class="token operator">=</span> first_paragraph<span class="token operator">|</span>last <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">set</span> last_three <span class="token operator">=</span> first_paragraph<span class="token operator">|</span>slice<span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">3</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">set</span> last_nine <span class="token operator">=</span> first_paragraph<span class="token operator">|</span>slice<span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">9</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%}</span></span><br><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span><br>    <span class="token tag-name keyword">if</span> last_char <span class="token operator">not</span> <span class="token operator">in</span> <span class="token punctuation">[</span><span class="token string"><span class="token punctuation">'</span>.<span class="token punctuation">'</span></span><span class="token punctuation">,</span> <span class="token string"><span class="token punctuation">'</span>!<span class="token punctuation">'</span></span><span class="token punctuation">,</span> <span class="token string"><span class="token punctuation">'</span>?<span class="token punctuation">'</span></span><span class="token punctuation">,</span> <span class="token string"><span class="token punctuation">'</span>…<span class="token punctuation">'</span></span><span class="token punctuation">,</span> <span class="token string"><span class="token punctuation">'</span>:<span class="token punctuation">'</span></span><span class="token punctuation">]</span> <span class="token operator">and</span><br>    last_three <span class="token operator">!=</span> <span class="token string"><span class="token punctuation">'</span>[…]<span class="token punctuation">'</span></span> <span class="token operator">and</span><br>    first_paragraph<span class="token operator">|</span>slice<span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">2</span><span class="token punctuation">)</span> <span class="token operator">!=</span> <span class="token string"><span class="token punctuation">'</span>."<span class="token punctuation">'</span></span> <span class="token operator">and</span><br>    first_paragraph<span class="token operator">|</span>slice<span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">6</span><span class="token punctuation">)</span> <span class="token operator">!=</span> <span class="token string"><span class="token punctuation">'</span>&amp;nbsp;<span class="token punctuation">'</span></span> <span class="token operator">and</span><br>    last_nine <span class="token operator">!=</span> <span class="token string"><span class="token punctuation">'</span> ... more<span class="token punctuation">'</span></span><br>  <span class="token delimiter punctuation">%}</span></span><br>    <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">set</span> first_paragraph <span class="token operator">=</span> first_paragraph <span class="token operator">~</span> <span class="token string"><span class="token punctuation">'</span>.<span class="token punctuation">'</span></span> <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">elseif</span> last_char <span class="token operator">==</span> <span class="token string"><span class="token punctuation">'</span>…<span class="token punctuation">'</span></span> <span class="token delimiter punctuation">%}</span></span><br>    <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">set</span> first_paragraph <span class="token operator">=</span> first_paragraph<span class="token operator">|</span>custom_replace<span class="token punctuation">(</span><span class="token string"><span class="token punctuation">'</span>/(?&lt;!\s)…$/u<span class="token punctuation">'</span></span><span class="token punctuation">,</span> <span class="token string"><span class="token punctuation">'</span> […]<span class="token punctuation">'</span></span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">elseif</span> last_nine <span class="token operator">==</span> <span class="token string"><span class="token punctuation">'</span> ... more<span class="token punctuation">'</span></span> <span class="token delimiter punctuation">%}</span></span><br>    <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">set</span> first_paragraph <span class="token operator">=</span> first_paragraph<span class="token operator">|</span>custom_replace<span class="token punctuation">(</span><span class="token string"><span class="token punctuation">'</span>/ \.\.\. more$/<span class="token punctuation">'</span></span><span class="token punctuation">,</span> <span class="token string"><span class="token punctuation">'</span> […]<span class="token punctuation">'</span></span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">elseif</span> last_three <span class="token operator">==</span> <span class="token string"><span class="token punctuation">'</span>...<span class="token punctuation">'</span></span> <span class="token delimiter punctuation">%}</span></span><br>    <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">set</span> first_paragraph <span class="token operator">=</span> first_paragraph<span class="token operator">|</span>custom_replace<span class="token punctuation">(</span><span class="token string"><span class="token punctuation">'</span>/(?&lt;!\s)\.{3}$/u<span class="token punctuation">'</span></span><span class="token punctuation">,</span> <span class="token string"><span class="token punctuation">'</span> […]<span class="token punctuation">'</span></span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">elseif</span> last_char <span class="token operator">==</span> <span class="token string"><span class="token punctuation">'</span>:<span class="token punctuation">'</span></span> <span class="token delimiter punctuation">%}</span></span><br>    <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">set</span> first_paragraph <span class="token operator">=</span> first_paragraph<span class="token operator">|</span>custom_replace<span class="token punctuation">(</span><span class="token string"><span class="token punctuation">'</span>/:$/<span class="token punctuation">'</span></span><span class="token punctuation">,</span> <span class="token string"><span class="token punctuation">'</span>.<span class="token punctuation">'</span></span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">endif</span> <span class="token delimiter punctuation">%}</span></span><br><br>  <span class="token twig language-twig"><span class="token comment">{# Hide descriptions that include these strings #}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">set</span> excluded_patterns <span class="token operator">=</span> <span class="token punctuation">[</span><br>    <span class="token string"><span class="token punctuation">'</span>©<span class="token punctuation">'</span></span><span class="token punctuation">,</span><br>    <span class="token string"><span class="token punctuation">'</span>Image courtesy of<span class="token punctuation">'</span></span><span class="token punctuation">,</span><br>    <span class="token string"><span class="token punctuation">'</span>Image supplied by<span class="token punctuation">'</span></span><span class="token punctuation">,</span><br>    <span class="token string"><span class="token punctuation">'</span>Image credit<span class="token punctuation">'</span></span><span class="token punctuation">,</span><br>    <span class="token string"><span class="token punctuation">'</span>The post<span class="token punctuation">'</span></span><span class="token punctuation">,</span><br>    <span class="token string"><span class="token punctuation">'</span>Image:<span class="token punctuation">'</span></span><span class="token punctuation">,</span><br>    <span class="token string"><span class="token punctuation">'</span>No abstract<span class="token punctuation">'</span></span><span class="token punctuation">,</span><br>    <span class="token string"><span class="token punctuation">'</span>If you enjoyed this episode<span class="token punctuation">'</span></span><span class="token punctuation">,</span><br>    <span class="token string"><span class="token punctuation">'</span>Published on<span class="token punctuation">'</span></span><br>  <span class="token punctuation">]</span> <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">set</span> is_excluded <span class="token operator">=</span> excluded_patterns<span class="token operator">|</span>filter<span class="token punctuation">(</span>pattern <span class="token operator">=</span><span class="token operator">></span> pattern <span class="token operator">in</span> first_paragraph<span class="token punctuation">)</span><span class="token operator">|</span>length <span class="token operator">></span> <span class="token number">0</span> <span class="token delimiter punctuation">%}</span></span><br><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">if</span> first_paragraph<span class="token operator">|</span>length <span class="token operator">></span> <span class="token number">5</span> <span class="token operator">and</span> <span class="token operator">not</span> is_excluded <span class="token delimiter punctuation">%}</span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>views-field views-field-description<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> first_paragraph<span class="token operator">|</span>custom_replace<span class="token punctuation">(</span><span class="token string"><span class="token punctuation">'</span>/&amp;nbsp;/<span class="token punctuation">'</span></span><span class="token punctuation">,</span> <span class="token string"><span class="token punctuation">'</span><span class="token punctuation">'</span></span><span class="token punctuation">)</span><span class="token operator">|</span>custom_replace<span class="token punctuation">(</span><span class="token string"><span class="token punctuation">'</span>/&amp;amp;/<span class="token punctuation">'</span></span><span class="token punctuation">,</span> <span class="token string"><span class="token punctuation">'</span>&amp;<span class="token punctuation">'</span></span><span class="token punctuation">)</span> <span class="token delimiter punctuation">}}</span></span><br>    <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">if</span> field_include_feed_description <span class="token operator">==</span> <span class="token string"><span class="token punctuation">'</span>1<span class="token punctuation">'</span></span> <span class="token delimiter punctuation">%}</span></span><br>      <span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> description_1 <span class="token delimiter punctuation">}}</span></span><br>    <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">endif</span> <span class="token delimiter punctuation">%}</span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">endif</span> <span class="token delimiter punctuation">%}</span></span><br><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>meta<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>via <span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> field_website <span class="token delimiter punctuation">}}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">if</span> field_donate <span class="token delimiter punctuation">%}</span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>time<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> timestamp <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span><span class="token punctuation">></span></span> <span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> field_donate <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">else</span> <span class="token delimiter punctuation">%}</span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>time<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> timestamp <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">endif</span> <span class="token delimiter punctuation">%}</span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>comment fade-in-quick<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> field_comment <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>Most of the logic here is a bunch of heuristics which tidy up the HTML contained within the RSS feeds, eg standardising the ellipsis style for truncated descriptions, and removing descriptions that don’t provide any value to the reader.</p>
<p>You’ll see in the screenshot that a number of fields are set to ‘hidden’. Hiding a field makes its value available to output in the ‘Rewrite results’ section of subsequent fields. This allows you to combine or perform logic on two or more fields at once. The Field ‘Aggregator feed item: Title’, for instance, uses both the ‘Link’ and ‘Podcast?’ fields. The following markup is from the ‘Title’ field configuration under ‘Rewrite results’:</p>
<pre class="language-twig"><code class="language-twig"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>news-item <span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> title_1 <span class="token delimiter punctuation">}}</span></span> fade-in-quick<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h3</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>headline<span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">if</span> field_podcast <span class="token operator">==</span> <span class="token string"><span class="token punctuation">'</span>1<span class="token punctuation">'</span></span> <span class="token delimiter punctuation">%}</span></span> icon-podcast<span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">endif</span><span class="token delimiter punctuation">%}</span></span><span class="token punctuation">"</span></span> <span class="token attr-name">iid</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> iid <span class="token delimiter punctuation">}}</span></span><span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> link <span class="token delimiter punctuation">}}</span></span><span class="token punctuation">"</span></span> <span class="token attr-name">target</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>_blank<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> title <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h3</span><span class="token punctuation">></span></span></code></pre>
<p>Here’s a screenshot of the UI for the ‘Title’ field:</p>
<figure class="w-950">
  <img src="/img/arfyi-configure-title.png" width="950" height="909" alt="Screenshot of the ‘Aggregator feed item: Title’ field UI.">
  <figcaption>Hidden fields ‘Link’ and ‘Podcast?’ (see inset, taken from the <a href="#heading-the-view">main Views screenshot</a>) are subsequently available in the ‘Aggregator feed item: Title’ field UI.</figcaption>
</figure>
<h3 id="best-practice">Best practice</h3>
<p>It’s probably better practice to create a Twig template file <a href="https://www.drupal.org/docs/develop/theming-drupal/twig-in-drupal/twig-template-naming-conventions#s-views">override</a> instead of scattering the template across the View’s GUI as I’ve done here. Using the GUI is great for quickly prototyping Views, but you may later want to port it to a Twig template file so you can see the full template at a glance. (I may do this for my View as part of a project refinement exercise.)</p>
<p>Having all the code in a single template file also makes it easy to track changes. That said, it’s still possible to track changes with the GUI approach by exporting the <a href="https://drupalize.me/tutorial/overview-configuration-system">configuration</a> (<code>drush cex</code>) after making changes then committing the output to git.</p>
<h3 id="filtering-the-view-output">Filtering the View output</h3>
<p>The ‘Filter criteria’ section removes certain items entirely, such as sponsored posts and recipes. ‘Filter headlines (exposed)’ provides a text field by the which the user can search to filter news items by their headline.</p>
<h3 id="accessing-feed-custom-fields-with-relationships">Accessing feed custom fields with Relationships</h3>
<p>The View is set up to list Aggregator feed items specifically; not the actual feeds. The ‘Aggregator feed’ relationship (under Advanced in the top-right of the <a href="#heading-the-view">Views screenshot</a>) allows us to include custom fields from Aggregator feeds themselves. These include ‘Link’ fields for the feed’s website and a page where the user can donate to or otherwise support the website. Here’s a screenshot of <code>/admin/config/services/aggregator/fields</code>:</p>
<figure class="w-950">
  <img src="/img/arfyi-feed-metadata.png" width="950" height="490" alt="Screenshot showing custom fields added to Aggregator feeds.">
  <figcaption>There are a few custom fields added to Aggregator feeds. The View is set to list Aggregator <i>items</i>, as opposed to <i>feeds</i>, but we can output fields from an item’s associated feed by adding a Relationship in the View’s Advanced section.</figcaption>
</figure>
<p>The ‘field_aggregator_item_rss_item_metadata’ Relationship links the View to a custom content type called ‘RSS Item Metadata’, which allows us to attach additional metadata to individual feed items. An ‘Entity reference’ field allows us to search for the news item we want. We can also add a comment beneath a news item or pin it to the right-hand column. Here’s the ‘Manage fields’ UI:</p>
<figure class="w-950">
  <img src="/img/arfyi-item-metadata.png" width="950" height="499" alt="Screenshot of the ‘RSS Item Metadata’ content type.">
  <figcaption>The ‘RSS Item Metadata’ content type allows us to choose a news item and comment on or ‘pin’ it.</figcaption>
</figure>
<h3 id="add-some-ajax">Add some AJAX</h3>
<p>Make sure ‘Use AJAX’ (under ‘Other’) is set to ‘Yes’. This allows for pagination and filtering (‘Filter by headline’) without reloading the entire page.</p>
<h2 id="refreshing-the-feeds">Refreshing the feeds</h2>
<p>In order to fetch new items as they’re published, I’ve set a <a href="/posts/running-scripts-as-cron-jobs/">cron job</a> to run every 10 minutes. Drupal’s automated cron system only <a href="https://www.drupal.org/docs/administering-a-drupal-site/cron-automated-tasks/cron-automated-tasks-overview#s-downsides-of-drupal-cron">runs</a> when Drupal processes a request, such as when someone visits the site, so I’ve <a href="https://www.drupal.org/docs/administering-a-drupal-site/cron-automated-tasks/cron-automated-tasks-overview#s-disabledrupal-cron">disabled</a> it by setting the ‘Run cron every’ dropdown at <code>/admin/config/system/cron</code> to ‘Never’, and added a cron directive via my hosting’s cron UI. It looks something like this (<code>__string__</code> is available at <code>/admin/config/system/cron</code>):</p>
<pre class="language-shell"><code class="language-shell">*/10 * * * * <span class="token function">curl</span> -s -o /dev/null https://animalrights.fyi/cron/__string__</code></pre>
<p>Using an external cron trigger in this way means the cron is guaranteed to run every 10 minutes.</p>
<p>There is an ‘Update interval’ field on each feed (<code>/aggregator/sources/FEED_ID/configure</code>), which I set to ‘15 mins’, ‘1 hour’, ‘1 day’ etc., depending on how frequently the given website tends to post new content. When the cron runs, it checks when each feed was last refreshed, and if the time elapsed since the last refresh exceeds the update interval, the feed will be fetched again, and any new items will be displayed by the View.</p>
<p>Additionally, news items older than a year are deleted, via the directive <code>$config['aggregator.settings']['items']['expire'] = 31536000;</code> in <code>settings.php</code>. (The ‘Discard items older than’ field at <code>/admin/config/services/aggregator/settings</code> is set to ‘Never’, and is overridden by <code>settings.php</code>.)</p>
<h2 id="conclusion">Conclusion</h2>
<p>This post is an overview rather than a step-by-step tutorial. I may explore specific aspects of the website’s functionality in more depth in future posts.</p>
<p>Concerning Drupal as a platform, I wouldn’t necessarily recommend it for side projects like this. Drupal is complex, which is fine because it’s powerful; but managing the system (mainly running regular updates and <a href="https://drupalize.me/course/configuration-management-drupal">managing configuration</a>) can be a massive ball-ache. I only chose it for AnimalRights.fyi because I already build Drupal sites professionally. (And <a href="https://en.wikipedia.org/wiki/Large_language_model">LLMs</a> like Claude and ChatGPT make working with Drupal and similarly complex platforms much easier.)</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>TIL: ‘Hello, world’ in Z80 assembly language on the ZX Spectrum</title>
      <link href="https://www.jamieonkeys.dev/posts/til-z80-asm-hello-world/"/>
      <updated>2024-02-09T00:00:00+00:00</updated>
      <id>https://www.jamieonkeys.dev/posts/til-z80-asm-hello-world/</id>
      <content type="html">
        <![CDATA[
      <p>I recently received my <a href="https://www.specnext.com">ZX Spectrum Next</a> after having backed a Kickstarter in 2020. I began my computing life on a <a href="https://www.dataserve-retro.co.uk/contents/en-uk/d66.html">Spectrum +2A</a> in 1989, and it’s also the machine I first learned to program on, in BASIC. I knew you could write things ‘in machine code’ (like most games produced for the platform were) but figuring it out was way over my head at the time. (Programs written in machine code are much faster and have access to more memory, but are low level and more difficult to write.)</p>
<p>Now that I have ZX Spectrum hardware for the first time in a couple of decades, I thought it would be a good time to see whether I could create a ‘Hello, world’ program in Z80 assembly language, convert it to machine code, and run it.</p>
<blockquote>
<p>Reader caution: I don’t really know what I’m doing here. This blog post is more or less personal documentation. I’ve tried to avoid inaccuracies, but feel free to correct me in the comments.</p>
</blockquote>
<p>I found the take on ‘Hello, world’ described on <a href="https://benjamin.computer/posts/2022-04-22-ZX-coding.html">Benjamin Blundell’s website</a> to be nice and succinct, so I used that. Here it is:</p>
<pre class="language-wasm"><code class="language-wasm">org <span class="token variable">$8000</span><br>  ld bc, MY_STRING<br><br>MY_LOOP:<br>  ld a, <span class="token punctuation">(</span>bc<span class="token punctuation">)</span><br>  cp <span class="token number">0</span><br>  jr z, END_PROGRAM<br>  rst <span class="token variable">$10</span><br>  inc bc<br>  jr MY_LOOP<br><br>END_PROGRAM:<br>  ret<br><br>MY_STRING:<br>  defb <span class="token string">"Hello, world!"</span><br>  defb <span class="token number">13</span>, <span class="token number">0</span></code></pre>
<p>I’ll use the Pasmo assembler to translate the instructions into binary machine code so that the Spectrum can understand it.</p>
<p>Some notes, based on Benjamin’s blog post, on what each line does:</p>
<h3 id="org-%248000"><code>org $8000</code></h3>
<p>This is an assembler directive to set the starting (‘origin’) memory location (address) of the proceeding program (<code>$8000</code> is 0x8000, or 8000 in hex, or decimal 32768). This location is safely within a 48K Spectrum’s non-system memory. (See <a href="http://www.breakintoprogram.co.uk/hardware/computers/zx-spectrum/memory-map">Dean Belfield’s website</a> for details on how memory is mapped in the both 48 and 128K models.)</p>
<h3 id="ld-bc%2C-my_string"><code>ld bc, MY_STRING</code></h3>
<p>Load register pair BC with the starting address of <code>MY_STRING</code>. <a href="https://www.totalphase.com/blog/2023/05/what-is-register-in-cpu-how-does-it-work/#:~:text=Registers%20are%20a%20type%20of,bit%20sequence%20or%20individual%20characters)">Registers</a> are small storage areas on the processor where data can be manipulated. We need to move a given value stored in memory into a register before anything can be done with it (eg print it on screen, do a calculation with it). We don’t need to think about this when working in a higher-level language like BASIC or JavaScript: memory management is handled for us. However when writing assembly language we need to move values explicitly between memory and the registers.</p>
<p>Regarding the address of our string, you may be wondering, as I did: ‘Hello, world!’ isn’t actually written to memory until later in the code, so how does the assembler know at this stage what the address is? The answer is that most assemblers, including Pasmo, run two passes, the first of which will read through the assembly code to determine the address of each label. Only on the second pass will the machine code be generated. So when the instruction <code>ld bc, MY_STRING</code> is encountered on pass two, the assembler knows the starting address of <code>MY_STRING</code>. This concept and process is referred to as ‘forward referencing’.</p>
<h3 id="my_loop%3A"><code>MY_LOOP:</code></h3>
<p>Add label <code>MY_LOOP</code> to mark a loop that will cycle through each byte of whatever is stored starting at location <code>MY_STRING</code>.</p>
<h3 id="ld-a%2C-(bc)"><code>ld a, (bc)</code></h3>
<p>Load register A (<a href="https://www.msx.org/wiki/Z80_Assembler_for_Dummies#The_registers">the accumulator</a>) with the value of the first byte at address <code>MY_STRING</code> (which will be the ‘H’ from ‘Hello, world!’ on the first go-around of this loop).</p>
<h3 id="cp-0"><code>cp 0</code></h3>
<p>Compare the contents of register A with 0. 0 is the last byte, after <code>Hello, world!</code> and a carriage return, set by the instruction <code>defb 13, 0</code> later.</p>
<h3 id="jr-z%2C-end_program"><code>jr z, END_PROGRAM</code></h3>
<p>Jump to <code>END_PROGRAM</code> if the above comparison is true (ie the value in A is 0).</p>
<h3 id="rst-%2410"><code>rst $10</code></h3>
<p>Call the ROM routine at address 0x10, which prints whatever is in register A to the screen. <code>RST</code> (or ‘restart’) is the same as <a href="http://z80-heaven.wikidot.com/instructions-set:call"><code>CALL</code></a>, except it uses only one byte, and is faster. However, you can only use it with eight specific ROM addresses, one of which is 0x10, which we call here. (Source: ‘Spectrum Machine Language for the Absolute Beginner’, page 128.) You could think of these routines as built-in helper functions which perform a specific task.</p>
<h3 id="inc-bc"><code>inc bc</code></h3>
<p>Increment register pair BC so it moves to the next byte in memory (ie the next character in our string).</p>
<h3 id="jr-my_loop"><code>jr MY_LOOP</code></h3>
<p>Jump back to the top of the loop.</p>
<h3 id="end_program%3A"><code>END_PROGRAM:</code></h3>
<p>Another label. Labels can have arbitrary values, and help us and the assembler navigate the code.</p>
<h3 id="ret"><code>ret</code></h3>
<p>Exit program.</p>
<h3 id="defb-%22hello%2C-world!%22"><code>defb &quot;Hello, world!&quot;</code></h3>
<p>Define bytes with the string we want to print.</p>
<h3 id="defb-13%2C-0"><code>defb 13, 0</code></h3>
<p>Define two more bytes — a carriage return (13) and string terminator (0) — so that <code>cp 0</code> earlier in the code can check whether we’ve reached the end of the string.</p>
<p>I saved the file as <code>helloworld.asm</code>.</p>
<h2 id="converting-our-program-into-machine-code">Converting our program into machine code</h2>
<p>We’ll use the command line tool Pasmo (a Z80 cross-assembler) to assemble our machine code and create our <code>.tap</code> file, which we can then run on a Spectrum emulator or actual Speccy hardware. The TAP will comprise <code>helloworld.asm</code> in machine code form, and a BASIC loader program. The latter loads the machine code into memory and runs it. (Alternative assemblers include <a href="https://github.com/Megatokio/zasm">zasm</a>, and <a href="https://www.specnext.com/software/?title=108">Odin</a> and <a href="https://spectrumcomputing.co.uk/entry/4000098/Timex/Zeus_Assembler">Zeus</a> if you’d like to develop on an actual Spectrum.)</p>
<h3 id="build-and-install-pasmo">Build and install Pasmo</h3>
<p>I’m using macOS here, but the steps should be similar on other platforms.</p>
<ol>
<li>First install <a href="https://cmake.org/download/">CMake</a>, a C++ build tool. I’ll use Homebrew: <code>brew install cmake</code></li>
<li>Download Pasmo: <code>git clone https://github.com/jounikor/pasmo.git</code></li>
<li><code>cd pasmo/pasmo</code> (steps 3 to 7 are taken from the Pasma <code>README.md</code>)</li>
<li><code>mkdir build</code></li>
<li><code>cd build</code></li>
<li><code>cmake ../</code></li>
<li><code>make</code></li>
<li>Copy Pasmo to <code>/usr/local/bin/</code> so that you can run it from anywhere: <code>sudo cp pasmo /usr/local/bin/</code></li>
<li>Relaunch the shell: <code>exec zsh -l</code></li>
<li>Verify that you can run Pasmo by viewing the manual page: <code>pasmo man</code></li>
</ol>
<h3 id="assemble-our-program-and-create-the-.tap-file">Assemble our program and create the <code>.tap</code> file</h3>
<ol>
<li>Before we run the assembly process, add the Pasmo directive <code>END $8000</code> at the end of your <code>helloworld.asm</code> file. This will prompt Pasmo to include a <code>RANDOMIZE USR</code> statement in the BASIC loader program. This will ensure our machine code runs when we launch the <code>.tap</code> file</li>
<li>Run <code>pasmo --tapbas helloworld.asm helloworld.tap</code></li>
</ol>
<p>Open <code>helloworld.tap</code> in Fuse or another emulator, or on an actual Spectrum Next, as I did. It should look like this:</p>
<figure>
  <img src="/img/helloworld-scaledup.jpg" width="714" height="536" alt="Screenshot of the ‘Hello, world’ program running on a Sinclair ZX Spectrum Next">
  <figcaption>Screenshot of the ‘Hello, world’ program running on a Sinclair ZX Spectrum Next. The original <code>.src</code> file was converted to a PNG with Remy Sharp’s <a href="https://zx.remysharp.com/tools/">image and font conversion tool</a>, then that 256×192 file was upscaled to 1920×1440 with Pixelmator Pro’s <a href="https://www.pixelmator.com/support/guide/pixelmator-pro/1006/">nearest-neighbour algorithm</a>.</figcaption>
</figure>
<h3 id="the-basic-loader-program">The BASIC loader program</h3>
<p>This is the BASIC loader program that Pasmo created:</p>
<pre class="language-purebasic"><code class="language-purebasic"><span class="token number">10</span> CLEAR <span class="token number">32767</span><br><span class="token number">20</span> POKE <span class="token number">23610</span><span class="token punctuation">,</span><span class="token number">255</span><br><span class="token number">30</span> LOAD <span class="token string">""</span> CODE<br><span class="token number">40</span> RANDOMIZE USR <span class="token number">32768</span></code></pre>
<p>Line by line:</p>
<ul>
<li><code>10 CLEAR 32767</code> — ensure that the BASIC interpreter doesn’t write to memory above address 32767 (as well as clear any variables that are already stored there). This value should be a byte before the start of where our machine code will be stored.</li>
<li><code>20 POKE 23610,255</code> — ‘avoid a[n] error message when using +3 loader’. (Source: <a href="https://github.com/jounikor/pasmo/blob/master/pasmo/spectrum.cxx#L159-L185"><code>spectrum.cxx</code></a> in the pasmo-0.5.5 codebase.)</li>
<li><code>30 LOAD &quot;&quot; CODE</code> — load the next binary code file the Spectrum finds (which would typically have been on a cassette tape back in the day, located just after the BASIC loader program).</li>
<li><code>40 RANDOMIZE USR 32768</code>: call (run) the machine code that’s stored starting at address 32768, which we specified in our <code>helloworld.asm</code> code on line 1 (<code>org $8000</code>), and at the end (<code>END $8000</code>) as Pasmo requires. <code>$8000</code> is a shorthand for hexadecimal 8000 (0x8000), or decimal 32768.</li>
</ul>
<h2 id="what-next%3F">What next?</h2>
<p>I just received my copy of <a href="https://hueygames.com/40-bmcr-zx-spectrum">40 Best Machine Code Routines for the ZX Spectrum</a> by John Hardman and Andrew Hewson (with a new chapter on the Next by Jim Bagley), and it seems like an accessible guide to using machine code. I’ll try out some of the routines.</p>
<p>The tutorial <a href="https://chuntey.wordpress.com/2010/01/12/tutorial-zx-spectrum-machine-code-game-30-minutes">‘ZX Spectrum Machine Code Game in 30 Minutes!’</a> by Jon Kingsman looks intruiging, and promises the ability to program in machine code in the time it takes to consume a large cup of tea. I tend to ‘learn by doing’ so I think I may tackle this next.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://benjamin.computer/posts/2022-04-22-ZX-coding.html">Assembly on the ZX Spectrum - Part 1, Benjamin Blundel</a></li>
<li>Original source of this ‘Hello, world’ code may have been <a href="https://github.com/DIDIx13/hello-world/blob/master/ASSEMBLER-Z80-ZXSPECTRUM">DIDIx13/hello-world</a></li>
<li><a href="https://www.amazon.co.uk/Spectrum-Machine-Language-Absolute-Beginner/dp/1789822378">Spectrum Machine Language for the Absolute Beginner</a>, ed. William Tang. 1982 (2020 reprint)</li>
<li>Jonathan Cauldwell, <a href="https://jonathan-cauldwell.itch.io/how-to-write-spectrum-games">How to Write ZX Spectrum Games</a>, v1.1 (?2020): ‘Chapter Eighteen - Making Games Load and Run Automatically’</li>
<li><a href="http://www.breakintoprogram.co.uk/hardware/computers/zx-spectrum/memory-map">Spectrum memory map</a>, Dean Belfield</li>
<li><a href="https://pasmo.speccy.org/pasmodoc.html">Pasmo docs</a></li>
<li>ChatGPT (GPT-4)</li>
</ul>
<hr>
<p class="footnotes">Featured post: <a href="/posts/freddie-mercury-lost-songs/">Hand-written lyrics for lost Freddie Mercury songs up for auction</a>.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Get a local instance of Drupal up and running fast with DDEV</title>
      <link href="https://www.jamieonkeys.dev/posts/drupal-ddev-mac/"/>
      <updated>2023-06-08T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/drupal-ddev-mac/</id>
      <content type="html">
        <![CDATA[
      <p>This is the quickest way I’ve found to get a local instance of Drupal up and running on macOS. (Deploying to a remote server isn’t covered in this post.)</p>
<h2 id="requirements">Requirements</h2>
<p>If you have not already done so, install the following tools:</p>
<ol>
<li><a href="https://docs.docker.com/desktop/mac/install/">Docker Desktop</a></li>
<li><a href="https://brew.sh">Homebrew</a>: <code>/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;</code></li>
<li><a href="https://getcomposer.org">Composer</a>: <code>brew install composer</code></li>
<li><a href="https://nodejs.org/en">Node</a>/<a href="https://www.npmjs.com">npm</a>: <code>brew install node</code></li>
<li><a href="https://ddev.com">DDEV</a>: <code>brew install ddev/ddev/ddev</code></li>
<li><a href="https://mkcert.org">mkcert</a>: <code>mkcert -install</code></li>
</ol>
<h2 id="installation">Installation</h2>
<p>I initially followed <a href="https://www.digitalocean.com/community/tutorials/how-to-develop-a-drupal-9-website-on-your-local-machine-using-docker-and-ddev">this Digital Ocean guide</a> and distilled the key steps into the below sequence. (The Digital Ocean tutorial also covers Linux.)</p>
<ol>
  <li><code>mkdir __project_name__</code></li>
  <li><code>cd __project_name__</code></li>
  <li><code>ddev config --project-type=drupal11 --docroot=web --create-docroot</code></li>
  <li><code>ddev start</code></li>
  <li><code>ddev composer create-project "drupal/recommended-project"</code>
    <ul>
      <li>This will install the latest stable version of Drupal. To choose an older version, say v10, run <code>ddev composer create-project "drupal/recommended-project:^10"</code>.</li>
    </ul>
  </li>
  <li><code>ddev composer require "drush/drush"</code>
    <ul>
      <li>If you get a PHP version compatibility error running this command:
        <ol>
          <li>Update <code>php_version</code> in <code>.ddev/config.yaml</code>.</li>
          <li><code>ddev restart</code></li>
          <li>Run step 6 again.</li>
        </ol>
      </li>
    </ul>
  <li><code>ddev exec drush site:install --account-name=admin --account-pass=admin</code> (or replace <code>admin</code> with a more secure username and password.)</li>
  <li><code>ddev launch</code></li>
</ol>
<p>Bish, bash, bosh: a new Drupal site, with the URL <code>https://__project_name__.ddev.site/</code>, should now open in your browser. Nae bother.</p>
<h2 id="that%E2%80%99s-it">That’s it</h2>
<p>You might like to install <a href="https://www.drupal.org/project/bootstrap5">Bootstrap5</a> as a starter theme: <code>composer require 'drupal/bootstrap5'</code>. And these modules:</p>
<ol>
<li><a href="https://www.drupal.org/project/simple_sitemap">Simple XML sitemap</a>: <code>composer require drupal/simple_sitemap &amp;&amp; ddev drush pm:enable simple_sitemap</code></li>
<li><a href="https://www.drupal.org/project/admin_toolbar">Admin Toolbar</a>: <code>composer require drupal/admin_toolbar &amp;&amp; ddev drush pm:enable admin_toolbar admin_toolbar_tools admin_toolbar_search</code></li>
<li><a href="https://www.drupal.org/project/pathauto">Pathauto</a>: <code>composer require drupal/pathauto &amp;&amp; ddev drush pm:enable pathauto</code></li>
</ol>
<p>At this point you might commit the code to a git repo, then get on with developing your site.</p>
<p>If you have any corrections or optimisations, please comment below.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Writing some JavaScript to download files from the web, with help from an AI chatbot</title>
      <link href="https://www.jamieonkeys.dev/posts/chatgpt-file-download-browser/"/>
      <updated>2023-04-30T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/chatgpt-file-download-browser/</id>
      <content type="html">
        <![CDATA[
      <p><a href="https://openai.com/blog/chatgpt">ChatGPT</a> really does supercharge your productivity as a developer. Code that would have taken maybe an hour or two (or more) to write and test using the power of human thought can now be produced by said human explaining the problem to ChatGPT and having it write the code.</p>
<p>For this blog post I’ve chosen a practical, real-life example from my archive of ChatGPT conversations. The problem in this case: I wanted to download a bunch of files (PDFs mostly) for which I had a list of URLs. My initial thought was to look into whether I could run something in the browser, for three reasons:</p>
<ol>
<li>I’m a front-end dev whose natural habitat is the browser, so I should be able to whip up a solution quickly.</li>
<li>You don’t need to worry about authentication (if the files are only accessible when you’re logged in) as you can run the code from within an authenticated browser session.</li>
<li>The code can be run by others easily on their own machines.</li>
</ol>
<p>On being presented with this problem, rather than start from a blank slate, I had a conversation with ChatGPT (GPT-4) about it. I’ve reproduced a lightly edited version of it below.</p>
<p>I’m aware that in blogging about a ChatGPT conversation I could be accused of being a bore (like somebody going on about the dream they had last night). However, large language model-based products, like ChatGPT and <a href="https://github.com/features/copilot">GitHub Copilot</a>, look like they’re ushering in a major change in how sofware developers work, so I thought why not write about how I’m using them.</p>
<p>ChatGPT maintains context throughout the conversation so I can ask it questions about the information it’s given me (<a href="#async">‘What does <code>async</code> do?’</a> and <a href="#destructuring">‘Is that called “destructuring”?’</a>), and ask it to refactor code to meet new requirements (<a href="#refactor">‘Can you refactor <code>downloadPDF()</code> to handle multiple PDFs?’</a>, <a href="#mime-type">‘Can I determine the MIME type?’</a>).</p>
<blockquote>
<p>If you’re in a hurry, you can <a href="#final">jump</a> straight to the final iteration of the code.</p>
</blockquote>
<hr>
<blockquote class="prompt-q">
<p>JavaScript function to fetch and download PDF.</p>
</blockquote>
<blockquote class="prompt-a w-990">
<p>To fetch and download a PDF using JavaScript, you can use the <code>fetch</code> function to get the PDF data as a Blob and then create a download link using that Blob. Here’s an example function that takes a URL and a filename as parameters:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">downloadPDF</span><span class="token punctuation">(</span><span class="token parameter">url<span class="token punctuation">,</span> filename</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>	<span class="token comment">// Fetch the PDF data as a Blob</span><br>	<span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span><br>	<span class="token keyword">const</span> blob <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">blob</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>	<span class="token comment">// Create a download link and set its attributes</span><br>	<span class="token keyword">const</span> link <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>	link<span class="token punctuation">.</span>href <span class="token operator">=</span> <span class="token constant">URL</span><span class="token punctuation">.</span><span class="token function">createObjectURL</span><span class="token punctuation">(</span>blob<span class="token punctuation">)</span><span class="token punctuation">;</span><br>	link<span class="token punctuation">.</span>download <span class="token operator">=</span> filename<span class="token punctuation">;</span><br><br>	<span class="token comment">// Append the link to the document, click it, and remove it</span><br>	document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>link<span class="token punctuation">)</span><span class="token punctuation">;</span><br>	link<span class="token punctuation">.</span><span class="token function">click</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>	document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">removeChild</span><span class="token punctuation">(</span>link<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Error while downloading PDF:'</span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// Usage example</span><br><span class="token keyword">const</span> pdfURL <span class="token operator">=</span> <span class="token string">'https://example.com/path/to/your/pdf-file.pdf'</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> pdfFilename <span class="token operator">=</span> <span class="token string">'downloaded-pdf-file.pdf'</span><span class="token punctuation">;</span><br><span class="token function">downloadPDF</span><span class="token punctuation">(</span>pdfURL<span class="token punctuation">,</span> pdfFilename<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Make sure to replace the <code>pdfURL</code> variable with the URL of the PDF file you want to download. The <code>pdfFilename</code> variable is the name that will be given to the downloaded file.</p>
</blockquote>
<p>[Tangential question removed.]</p>
<blockquote class="prompt-q" id="refactor">
<p>Re the <code>downloadPDF()</code> function you wrote, can you refactor to handle a list of PDFs to download, each with a specific, defined value[?]</p>
</blockquote>
<blockquote class="prompt-a w-990">
<p>Certainly! Here’s a modified version of the <code>downloadPDF</code> function that accepts an array of objects, each containing the URL and filename for a PDF. The function will download all PDFs one after another.</p>
<pre class="language-javascript"><code class="language-javascript"><mark class="highlight-line highlight-line-active"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">downloadPDFs</span><span class="token punctuation">(</span><span class="token parameter">pdfs</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token comment">// [Deleted]</span></mark><br><mark class="highlight-line highlight-line-active"></mark><br><mark class="highlight-line highlight-line-active">  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> pdf <span class="token keyword">of</span> pdfs<span class="token punctuation">)</span> <span class="token punctuation">{</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token keyword">const</span> <span class="token punctuation">{</span> url<span class="token punctuation">,</span> filename <span class="token punctuation">}</span> <span class="token operator">=</span> pdf<span class="token punctuation">;</span></mark><br><span class="highlight-line">	<span class="token keyword">try</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">	  <span class="token comment">// Fetch the PDF data as a Blob</span></span><br><span class="highlight-line">	  <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  <span class="token keyword">const</span> blob <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">blob</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><span class="highlight-line">	  <span class="token comment">// Create a download link and set its attributes</span></span><br><span class="highlight-line">	  <span class="token keyword">const</span> link <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  link<span class="token punctuation">.</span>href <span class="token operator">=</span> <span class="token constant">URL</span><span class="token punctuation">.</span><span class="token function">createObjectURL</span><span class="token punctuation">(</span>blob<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  link<span class="token punctuation">.</span>download <span class="token operator">=</span> filename<span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><span class="highlight-line">	  <span class="token comment">// Append the link to the document, click it, and remove it</span></span><br><span class="highlight-line">	  document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>link<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  link<span class="token punctuation">.</span><span class="token function">click</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">removeChild</span><span class="token punctuation">(</span>link<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><mark class="highlight-line highlight-line-active">	  <span class="token comment">// Add a delay to avoid triggering browser's security mechanisms</span></mark><br><mark class="highlight-line highlight-line-active">	  <span class="token keyword">await</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">resolve</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">setTimeout</span><span class="token punctuation">(</span>resolve<span class="token punctuation">,</span> <span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></mark><br><span class="highlight-line">	<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><mark class="highlight-line highlight-line-active">	  console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Error while downloading </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>filename<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><span class="token punctuation">;</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token punctuation">}</span></mark><br><span class="highlight-line">  <span class="token punctuation">}</span></span><br><mark class="highlight-line highlight-line-active"><span class="token punctuation">}</span></mark><br><span class="highlight-line"></span><br><mark class="highlight-line highlight-line-active"><span class="token comment">// Usage example</span></mark><br><mark class="highlight-line highlight-line-active"><span class="token keyword">const</span> pdfList <span class="token operator">=</span> <span class="token punctuation">[</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token punctuation">{</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token literal-property property">url</span><span class="token operator">:</span> <span class="token string">'https://example.com/path/to/your/pdf-file1.pdf'</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token literal-property property">filename</span><span class="token operator">:</span> <span class="token string">'downloaded-pdf-file1.pdf'</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token punctuation">}</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token punctuation">{</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token literal-property property">url</span><span class="token operator">:</span> <span class="token string">'https://example.com/path/to/your/pdf-file2.pdf'</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token literal-property property">filename</span><span class="token operator">:</span> <span class="token string">'downloaded-pdf-file2.pdf'</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token punctuation">}</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token comment">// Add more PDFs as needed</span></mark><br><mark class="highlight-line highlight-line-active"><span class="token punctuation">]</span><span class="token punctuation">;</span></mark><br><span class="highlight-line"></span><br><mark class="highlight-line highlight-line-active"><span class="token function">downloadPDFs</span><span class="token punctuation">(</span>pdfList<span class="token punctuation">)</span><span class="token punctuation">;</span></mark></code></pre>
<p>Replace the <code>pdfList</code> array with the URLs and filenames for the PDF files you want to download. The function will iterate through the list and download each PDF file with the specified filename.</p>
<p>Please note that downloading multiple files in quick succession might be flagged by some browsers as suspicious behavior, potentially causing the downloads to be blocked. To avoid this, a delay is added between downloads using <code>setTimeout</code>. You can adjust the delay time as needed.</p>
</blockquote>
<blockquote class="prompt-q" id="async">
<p>What does the <code>async</code> before the function definition mean?</p>
</blockquote>
<blockquote class="prompt-a w-990">
<p>The <code>async</code> keyword before a function definition indicates that the function is asynchronous. An asynchronous function is a function that can perform operations without blocking the execution of other code, allowing other tasks to run concurrently.</p>
<p>Asynchronous functions use the <code>await</code> keyword within their bodies to pause the execution of the function until a Promise is resolved or rejected. When a Promise is resolved, the function continues execution from the point where it was paused.</p>
<p>Here’s a simple example to illustrate how async functions work:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">fetchData</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Fetching data...'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// Simulate an asynchronous operation (e.g., fetching data from an API)</span><br>  <span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">resolve</span><span class="token punctuation">)</span> <span class="token operator">=></span><br>	<span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">resolve</span><span class="token punctuation">(</span><span class="token string">'Data fetched!'</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">1000</span><span class="token punctuation">)</span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>result<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Starting...'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token function">fetchData</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Waiting for data...'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>The output will be:</p>
<pre class="language-plaintext"><code class="language-plaintext">Starting...<br>Fetching data...<br>Waiting for data...<br>Data fetched!</code></pre>
<p>As you can see, the async function does not block the execution of the rest of the code. While it’s waiting for the Promise to resolve, other code (in this case, the <code>console.log('Waiting for data...')</code>) can run.</p>
<p>In the <code>downloadPDFs</code> function example, the <code>async</code> keyword allows the function to wait for the fetching and downloading of each PDF without blocking the execution of other code. This is useful when working with network requests or other asynchronous operations.</p>
</blockquote>
<blockquote class="prompt-q" id="destructuring">
<p>In the refactored <code>downloadPDFs()</code> function, what does <code>const { url, filename } = pdf;</code> do? Is that called ‘destructuring’ or something?</p>
</blockquote>
<blockquote class="prompt-a w-990">
<p>Yes, you’re correct! The line <code>const { url, filename } = pdf;</code> is an example of object destructuring in JavaScript. Object destructuring is a syntax feature that allows you to extract properties from an object and create new variables that hold the values of these properties.</p>
<p>In the <code>downloadPDFs()</code> function, the <code>pdfs</code> parameter is an array of objects, where each object has a <code>url</code> and <code>filename</code> property. The line <code>const { url, filename } = pdf;</code> is used to extract the <code>url</code> and <code>filename</code> properties from the current <code>pdf</code> object in the loop. This creates two new variables <code>url</code> and <code>filename</code> holding the respective values, which can then be used in the following lines of code.</p>
<p>Here’s a simple example to illustrate object destructuring:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> person <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'John'</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">age</span><span class="token operator">:</span> <span class="token number">30</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> <span class="token punctuation">{</span> name<span class="token punctuation">,</span> age <span class="token punctuation">}</span> <span class="token operator">=</span> person<span class="token punctuation">;</span><br><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Output: 'John'</span><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>age<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// Output: 30</span></code></pre>
<p>In this example, the <code>name</code> and <code>age</code> properties are extracted from the <code>person</code> object, and new variables <code>name</code> and <code>age</code> are created with the corresponding values.</p>
</blockquote>
<blockquote class="prompt-q">
<p>Can you do this with arrays, for example if <code>pdfList</code> was an array of arrays, each PDF array being the format <code>['https://example.com/pdfs/pdf1.pdf', 'pdf1.pdf']</code>?</p>
</blockquote>
<blockquote class="prompt-a w-990">
<p>Yes, you can use array destructuring to handle the <code>pdfList</code> if it’s an array of arrays. Array destructuring works similarly to object destructuring but is used to extract elements from arrays. Here’s the modified version of the <code>downloadPDFs()</code> function that works with an array of arrays:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">downloadPDFs</span><span class="token punctuation">(</span><span class="token parameter">pdfs</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> pdf <span class="token keyword">of</span> pdfs<span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><mark class="highlight-line highlight-line-active">	<span class="token keyword">const</span> <span class="token punctuation">[</span>url<span class="token punctuation">,</span> filename<span class="token punctuation">]</span> <span class="token operator">=</span> pdf<span class="token punctuation">;</span> <span class="token comment">// Array destructuring</span></mark><br><span class="highlight-line">	<span class="token keyword">try</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">	  <span class="token comment">// Fetch the PDF data as a Blob</span></span><br><span class="highlight-line">	  <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  <span class="token keyword">const</span> blob <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">blob</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><span class="highlight-line">	  <span class="token comment">// Create a download link and set its attributes</span></span><br><span class="highlight-line">	  <span class="token keyword">const</span> link <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  link<span class="token punctuation">.</span>href <span class="token operator">=</span> <span class="token constant">URL</span><span class="token punctuation">.</span><span class="token function">createObjectURL</span><span class="token punctuation">(</span>blob<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  link<span class="token punctuation">.</span>download <span class="token operator">=</span> filename<span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><span class="highlight-line">	  <span class="token comment">// Append the link to the document, click it, and remove it</span></span><br><span class="highlight-line">	  document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>link<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  link<span class="token punctuation">.</span><span class="token function">click</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">removeChild</span><span class="token punctuation">(</span>link<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><span class="highlight-line">	  <span class="token comment">// Add a delay to avoid triggering browser's security mechanisms</span></span><br><span class="highlight-line">	  <span class="token keyword">await</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">resolve</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">setTimeout</span><span class="token punctuation">(</span>resolve<span class="token punctuation">,</span> <span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">	  console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Error while downloading </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>filename<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	<span class="token punctuation">}</span></span><br><span class="highlight-line">  <span class="token punctuation">}</span></span><br><span class="highlight-line"><span class="token punctuation">}</span></span><br><span class="highlight-line"></span><br><span class="highlight-line"><span class="token comment">// Usage example</span></span><br><span class="highlight-line"><span class="token keyword">const</span> pdfList <span class="token operator">=</span> <span class="token punctuation">[</span></span><br><mark class="highlight-line highlight-line-active">  <span class="token punctuation">[</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token string">'https://example.com/path/to/your/pdf-file1.pdf'</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token string">'downloaded-pdf-file1.pdf'</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token punctuation">]</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token punctuation">[</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token string">'https://example.com/path/to/your/pdf-file2.pdf'</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token string">'downloaded-pdf-file2.pdf'</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token punctuation">]</span><span class="token punctuation">,</span></mark><br><span class="highlight-line">  <span class="token comment">// Add more PDFs as needed</span></span><br><span class="highlight-line"><span class="token punctuation">]</span><span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><span class="highlight-line"><span class="token function">downloadPDFs</span><span class="token punctuation">(</span>pdfList<span class="token punctuation">)</span><span class="token punctuation">;</span></span></code></pre>
<p>In this version, the line <code>const [url, filename] = pdf;</code> uses array destructuring to extract the URL and filename from each array element in <code>pdfList</code>. The rest of the function remains the same.</p>
</blockquote>
<blockquote class="prompt-q" id="mime-type">
<p>Say some of the files were Word docs (<code>.docx</code>); can I determine the MIME type from the response so I can give the file to be downloaded the correct file extension?</p>
</blockquote>
<blockquote class="prompt-a w-990">
<p>Yes, you can determine the MIME type of the fetched file from the <code>Content-Type</code> header in the response object. Based on the MIME type, you can set the appropriate file extension for the downloaded file. Here’s the modified version of the <code>downloadPDFs()</code> function that handles different file types and sets the correct file extension:</p>
<pre class="language-javascript"><code class="language-javascript"><mark class="highlight-line highlight-line-active"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">downloadFiles</span><span class="token punctuation">(</span><span class="token parameter">files</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> file <span class="token keyword">of</span> files<span class="token punctuation">)</span> <span class="token punctuation">{</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token keyword">const</span> <span class="token punctuation">[</span>url<span class="token punctuation">,</span> filename<span class="token punctuation">]</span> <span class="token operator">=</span> file<span class="token punctuation">;</span></mark><br><span class="highlight-line">	<span class="token keyword">try</span> <span class="token punctuation">{</span></span><br><mark class="highlight-line highlight-line-active">	  <span class="token comment">// Fetch the file data as a Blob</span></mark><br><span class="highlight-line">	  <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  <span class="token keyword">const</span> blob <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">blob</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><mark class="highlight-line highlight-line-active">	  <span class="token comment">// Get the MIME type from the response headers</span></mark><br><mark class="highlight-line highlight-line-active">	  <span class="token keyword">const</span> contentType <span class="token operator">=</span> response<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'content-type'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></mark><br><mark class="highlight-line highlight-line-active">	  <span class="token keyword">let</span> extension<span class="token punctuation">;</span></mark><br><span class="highlight-line"></span><br><mark class="highlight-line highlight-line-active">	  <span class="token comment">// Determine the file extension based on the MIME type</span></mark><br><mark class="highlight-line highlight-line-active">	  <span class="token keyword">switch</span> <span class="token punctuation">(</span>contentType<span class="token punctuation">)</span> <span class="token punctuation">{</span></mark><br><mark class="highlight-line highlight-line-active">		<span class="token keyword">case</span> <span class="token string">'application/pdf'</span><span class="token operator">:</span></mark><br><mark class="highlight-line highlight-line-active">		  extension <span class="token operator">=</span> <span class="token string">'.pdf'</span><span class="token punctuation">;</span></mark><br><mark class="highlight-line highlight-line-active">		  <span class="token keyword">break</span><span class="token punctuation">;</span></mark><br><mark class="highlight-line highlight-line-active">		<span class="token keyword">case</span> <span class="token string">'application/vnd.openxmlformats-officedocument.wordprocessingml.document'</span><span class="token operator">:</span></mark><br><mark class="highlight-line highlight-line-active">		  extension <span class="token operator">=</span> <span class="token string">'.docx'</span><span class="token punctuation">;</span></mark><br><mark class="highlight-line highlight-line-active">		  <span class="token keyword">break</span><span class="token punctuation">;</span></mark><br><mark class="highlight-line highlight-line-active">		<span class="token comment">// Add more cases for other MIME types if needed</span></mark><br><mark class="highlight-line highlight-line-active">		<span class="token keyword">default</span><span class="token operator">:</span></mark><br><mark class="highlight-line highlight-line-active">		  extension <span class="token operator">=</span> <span class="token string">''</span><span class="token punctuation">;</span> <span class="token comment">// Fallback to no extension if MIME type is unknown</span></mark><br><mark class="highlight-line highlight-line-active">	  <span class="token punctuation">}</span></mark><br><span class="highlight-line"></span><br><span class="highlight-line">	  <span class="token comment">// Create a download link and set its attributes</span></span><br><span class="highlight-line">	  <span class="token keyword">const</span> link <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  link<span class="token punctuation">.</span>href <span class="token operator">=</span> <span class="token constant">URL</span><span class="token punctuation">.</span><span class="token function">createObjectURL</span><span class="token punctuation">(</span>blob<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><mark class="highlight-line highlight-line-active">	  link<span class="token punctuation">.</span>download <span class="token operator">=</span> filename <span class="token operator">+</span> extension<span class="token punctuation">;</span></mark><br><span class="highlight-line"></span><br><span class="highlight-line">	  <span class="token comment">// Append the link to the document, click it, and remove it</span></span><br><span class="highlight-line">	  document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>link<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  link<span class="token punctuation">.</span><span class="token function">click</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">removeChild</span><span class="token punctuation">(</span>link<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><span class="highlight-line">	  <span class="token comment">// Add a delay to avoid triggering browser's security mechanisms</span></span><br><span class="highlight-line">	  <span class="token keyword">await</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">resolve</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">setTimeout</span><span class="token punctuation">(</span>resolve<span class="token punctuation">,</span> <span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">	  console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Error while downloading </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>filename<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	<span class="token punctuation">}</span></span><br><span class="highlight-line">  <span class="token punctuation">}</span></span><br><span class="highlight-line"><span class="token punctuation">}</span></span><br><span class="highlight-line"></span><br><span class="highlight-line"><span class="token comment">// Usage example</span></span><br><mark class="highlight-line highlight-line-active"><span class="token keyword">const</span> fileList <span class="token operator">=</span> <span class="token punctuation">[</span></mark><br><span class="highlight-line">  <span class="token punctuation">[</span></span><br><span class="highlight-line">	<span class="token string">'https://example.com/path/to/your/pdf-file1.pdf'</span><span class="token punctuation">,</span></span><br><mark class="highlight-line highlight-line-active">	<span class="token string">'downloaded-pdf-file1'</span><span class="token punctuation">,</span></mark><br><span class="highlight-line">  <span class="token punctuation">]</span><span class="token punctuation">,</span></span><br><span class="highlight-line">  <span class="token punctuation">[</span></span><br><mark class="highlight-line highlight-line-active">	<span class="token string">'https://example.com/path/to/your/word-file1.docx'</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token string">'downloaded-word-file1'</span><span class="token punctuation">,</span></mark><br><span class="highlight-line">  <span class="token punctuation">]</span><span class="token punctuation">,</span></span><br><mark class="highlight-line highlight-line-active">  <span class="token comment">// Add more files as needed</span></mark><br><span class="highlight-line"><span class="token punctuation">]</span><span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><mark class="highlight-line highlight-line-active"><span class="token function">downloadFiles</span><span class="token punctuation">(</span>fileList<span class="token punctuation">)</span><span class="token punctuation">;</span></mark></code></pre>
<p>In this version, the function is renamed to <code>downloadFiles()</code> to reflect that it can handle different file types. The MIME type is obtained from the response headers using <code>response.headers.get('content-type')</code>, and a <code>switch</code> statement is used to determine the appropriate file extension based on the MIME type. The <code>link.download</code> attribute is set to the filename concatenated with the determined file extension.</p>
<p>Please note that this approach relies on the server providing the correct <code>Content-Type</code> header for the files. If the server doesn’t provide the correct MIME type, you might need to use an alternative method, such as checking the file extension in the URL or using a library to determine the file type based on the content.</p>
</blockquote>
<p><a id="final"></a>And that was me with the code I needed. I manually tidied it up a bit, and this is the format of the final version that I ran:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">downloadFiles</span><span class="token punctuation">(</span><span class="token parameter">files</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><span class="highlight-line"></span><br><mark class="highlight-line highlight-line-active">  <span class="token comment">// MIME type: file extension</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token keyword">const</span> fileExtensions <span class="token operator">=</span> <span class="token punctuation">{</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token string-property property">'application/pdf'</span><span class="token operator">:</span> <span class="token string">'pdf'</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token string-property property">'application/vnd.openxmlformats-officedocument.wordprocessingml.document'</span><span class="token operator">:</span> <span class="token string">'docx'</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token string-property property">'application/msword'</span><span class="token operator">:</span> <span class="token string">'doc'</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">	<span class="token string-property property">'application/zip'</span><span class="token operator">:</span> <span class="token string">'zip'</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token punctuation">}</span></mark><br><span class="highlight-line"></span><br><mark class="highlight-line highlight-line-active">  <span class="token keyword">const</span> mimeTypes <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span></mark><br><span class="highlight-line"></span><br><mark class="highlight-line highlight-line-active">  <span class="token comment">// Collate MIME types</span></mark><br><mark class="highlight-line highlight-line-active">  Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>fileExtensions<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">key</span> <span class="token operator">=></span> <span class="token punctuation">{</span></mark><br><mark class="highlight-line highlight-line-active">	mimeTypes<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>key<span class="token punctuation">)</span><span class="token punctuation">;</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></mark><br><span class="highlight-line"></span><br><span class="highlight-line">  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> file <span class="token keyword">of</span> files<span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">	<span class="token keyword">const</span> <span class="token punctuation">[</span>url<span class="token punctuation">,</span> filename<span class="token punctuation">]</span> <span class="token operator">=</span> file<span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><span class="highlight-line">	<span class="token keyword">try</span> <span class="token punctuation">{</span> <span class="token comment">// Fetch the file data as a Blob</span></span><br><span class="highlight-line"></span><br><span class="highlight-line">	  <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	  <span class="token keyword">const</span> contentType <span class="token operator">=</span> response<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'Content-Type'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><mark class="highlight-line highlight-line-active">	  <span class="token keyword">if</span> <span class="token punctuation">(</span>response<span class="token punctuation">.</span>ok <span class="token operator">&amp;&amp;</span> mimeTypes<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span>contentType<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></mark><br><span class="highlight-line">		<span class="token keyword">const</span> blob <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">blob</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><span class="highlight-line">		<span class="token comment">// Create a download link and set its attributes</span></span><br><span class="highlight-line">		<span class="token keyword">const</span> link <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">		link<span class="token punctuation">.</span>href <span class="token operator">=</span> <span class="token constant">URL</span><span class="token punctuation">.</span><span class="token function">createObjectURL</span><span class="token punctuation">(</span>blob<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><mark class="highlight-line highlight-line-active">		link<span class="token punctuation">.</span>download <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>filename<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>fileExtensions<span class="token punctuation">[</span>contentType<span class="token punctuation">]</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></mark><br><span class="highlight-line"></span><br><span class="highlight-line">		<span class="token comment">// Append the link to the document, click it, and remove it</span></span><br><span class="highlight-line">		document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>link<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">		link<span class="token punctuation">.</span><span class="token function">click</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">		document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">removeChild</span><span class="token punctuation">(</span>link<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><mark class="highlight-line highlight-line-active">	  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span></mark><br><mark class="highlight-line highlight-line-active">		console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Error while downloading </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>filename<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">: Invalid content type (</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>response<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'Content-Type'</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">) or response error</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span></mark><br><mark class="highlight-line highlight-line-active">	  <span class="token punctuation">}</span></mark><br><span class="highlight-line"></span><br><span class="highlight-line">	  <span class="token comment">// Add a delay to avoid triggering browser's security mechanisms</span></span><br><mark class="highlight-line highlight-line-active">	  <span class="token keyword">await</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">resolve</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">setTimeout</span><span class="token punctuation">(</span>resolve<span class="token punctuation">,</span> <span class="token number">300</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></mark><br><span class="highlight-line">	<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">	  console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Error while downloading </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>filename<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">	<span class="token punctuation">}</span></span><br><span class="highlight-line">  <span class="token punctuation">}</span></span><br><span class="highlight-line"><span class="token punctuation">}</span></span><br><span class="highlight-line"></span><br><span class="highlight-line"><span class="token comment">// Usage example</span></span><br><mark class="highlight-line highlight-line-active"><span class="token keyword">var</span> fileList <span class="token operator">=</span> <span class="token punctuation">[</span> <span class="token comment">// `const` → `var` so we run in single browser console session more than once</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token punctuation">[</span><span class="token string">'https://example.com/path/to/your/pdf-file1.pdf'</span><span class="token punctuation">,</span> <span class="token string">'pdf-file1'</span><span class="token punctuation">]</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active">  <span class="token punctuation">[</span><span class="token string">'https://example.com/path/to/your/word-file1.docx'</span><span class="token punctuation">,</span> <span class="token string">'word-file1'</span><span class="token punctuation">]</span><span class="token punctuation">,</span></mark><br><mark class="highlight-line highlight-line-active"><span class="token punctuation">]</span><span class="token punctuation">;</span></mark><br><span class="highlight-line"></span><br><span class="highlight-line"><span class="token function">downloadFiles</span><span class="token punctuation">(</span>fileList<span class="token punctuation">)</span><span class="token punctuation">;</span></span></code></pre>
<hr>
<p>This was a pretty straightforward series of prompts. The problem was a relatively simple one, and ChatGPT was able to output what I wanted from start to finish. In more complex scenarios, it might only get you started, or you’ll have enough code you want to ask it about that you’ll run out of <a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts-ai/tokens#:~:text=Tokens%20are%20the%20basic%20units,chosen%20tokenization%20method%20or%20scheme">tokens</a>. ChatGPT also didn’t have any <a href="https://www.phind.com/search?cache=7fa5a46f-58a7-4950-9560-e49fc18d5237">hallucinations</a> in this session. When that happens you can politely let it know it’s havering and should provide a new answer.</p>
<p>And as to the way I went about solving this problem (running a script from the browser console), some may think it a bit inelegant or hacky. You’re probably right! But it did the job and that’s what matters. And thanks to ChatGPT it took less time and effort than it would have otherwise.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Drupal how-to: list menu items, outputting a thumbnail image assigned to each item</title>
      <link href="https://www.jamieonkeys.dev/posts/drupal-menu-gallery/"/>
      <updated>2023-02-12T00:00:00+00:00</updated>
      <id>https://www.jamieonkeys.dev/posts/drupal-menu-gallery/</id>
      <content type="html">
        <![CDATA[
      <blockquote>
<p>Disclaimer: I’m not a Drupal expert and share this post only to help others who may have a similar use case. I make no guarantees of best practice! This should work in Drupal 8 onwards. Refinements welcome in the comments.</p>
</blockquote>
<p>The scenario here is that you want to create a number of landing pages which, within the body of each landing page, list their immediate sub-pages. Along with a hyperlink, you also want to output a thumbnail image which has been assigned to each sub-page.</p>
<p>Say you were building a website for a professional wrestling organisation called the Scottish Wrestling Federation (SWF) and had a number of categories under a parent <em>Roster</em> page: <em>Men</em>, <em>Women</em>, <em>Broadcast team</em> and <em>Referees</em>. Each landing page is a gallery of photos which link to a relevant profile page.</p>
<p>First create a custom <code>Image</code> field called <code>field_wrestler_photo</code> (most of the roster are wrestlers so we’ll ignore the fact that a few are referees and announcers in terms of the naming). Then set up a <code>Menu</code> block called <em>Sub-pages</em> within the <em>Main content</em> region at <code>/admin/structure/block</code>. You should now see a theme hook suggestion at <code>/admin/structure/block/manage/sub_pages#edit-settings-style</code> (within the <em>HTML and style options</em> settings). Let’s name the block <code>menu__sub_pages</code>.</p>
<p>Next add a function called <code>swf_preprocess_menu__sub_pages()</code> to our <code>swf.theme</code> file. This function will process the URL and alt text data and assign it to properties of <code>$variables</code>. The data will then be available to us in <code>menu--sub_pages.html.twig</code> in the <code>templates/navigation</code> folder.</p>
<p>We’ll be using the <code>Node</code> interface within our preprocess function in <code>swf.theme</code>, so first include it towards the top of the file:</p>
<pre class="language-php"><code class="language-php"><span class="token keyword">use</span> <span class="token package">Drupal<span class="token punctuation">\</span>node<span class="token punctuation">\</span>Entity<span class="token punctuation">\</span>Node</span><span class="token punctuation">;</span></code></pre>
<p>Begin our function by setting up a loop to get the data for each page:</p>
<pre class="language-php"><code class="language-php"><span class="token keyword">function</span> <span class="token function-definition function">swf_preprocess_menu__sub_pages</span><span class="token punctuation">(</span><span class="token operator">&amp;</span><span class="token variable">$variables</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token variable">$variables</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'items'</span><span class="token punctuation">]</span> <span class="token keyword">as</span> <span class="token variable">$key</span> <span class="token operator">=></span> <span class="token variable">$item</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Loop through each page</span><br>    <span class="token comment">// ...</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>For each page, get its URL (eg <code>/roster/women/jean-meikle</code>) and node path (eg <code>/node/23</code>). From the node path use <code>preg_match()</code> to get the node ID (<code>23</code> in this example):</p>
<pre class="language-php"><code class="language-php"><span class="highlight-line"><span class="token keyword">function</span> <span class="token function-definition function">swf_preprocess_menu__sub_pages</span><span class="token punctuation">(</span><span class="token operator">&amp;</span><span class="token variable">$variables</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">  <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token variable">$variables</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'items'</span><span class="token punctuation">]</span> <span class="token keyword">as</span> <span class="token variable">$key</span> <span class="token operator">=></span> <span class="token variable">$item</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><mark class="highlight-line highlight-line-active">    <span class="token variable">$page_url</span> <span class="token operator">=</span> <span class="token variable">$variables</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'items'</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token variable">$key</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'url'</span><span class="token punctuation">]</span><span class="token operator">-></span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Get page’s URL</span></mark><br><mark class="highlight-line highlight-line-active">    <span class="token variable">$node_path</span> <span class="token operator">=</span> <span class="token class-name class-name-fully-qualified static-context"><span class="token punctuation">\</span>Drupal</span><span class="token operator">::</span><span class="token function">service</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'path_alias.manager'</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">getPathByAlias</span><span class="token punctuation">(</span><span class="token variable">$page_url</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Get node path, eg /node/23</span></mark><br><mark class="highlight-line highlight-line-active">    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">preg_match</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'/node\/(\d+)/'</span><span class="token punctuation">,</span> <span class="token variable">$node_path</span><span class="token punctuation">,</span> <span class="token variable">$matches</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></mark><br><mark class="highlight-line highlight-line-active">      <span class="token variable">$node_id</span> <span class="token operator">=</span> <span class="token function">intval</span><span class="token punctuation">(</span><span class="token variable">$matches</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Get the numeric node ID</span></mark><br><mark class="highlight-line highlight-line-active">      <span class="token comment">// ...</span></mark><br><mark class="highlight-line highlight-line-active">    <span class="token punctuation">}</span></mark><br><span class="highlight-line">  <span class="token punctuation">}</span></span><br><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>Next, check for the property <code>field_wrestler_photo</code> and whether it has a value. Then get the <code>Node</code> object for this node ID:</p>
<pre class="language-php"><code class="language-php"><span class="highlight-line"><span class="token keyword">function</span> <span class="token function-definition function">swf_preprocess_menu__sub_pages</span><span class="token punctuation">(</span><span class="token operator">&amp;</span><span class="token variable">$variables</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">  <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token variable">$variables</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'items'</span><span class="token punctuation">]</span> <span class="token keyword">as</span> <span class="token variable">$key</span> <span class="token operator">=></span> <span class="token variable">$item</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">    <span class="token variable">$page_url</span> <span class="token operator">=</span> <span class="token variable">$variables</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'items'</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token variable">$key</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'url'</span><span class="token punctuation">]</span><span class="token operator">-></span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">    <span class="token variable">$node_path</span> <span class="token operator">=</span> <span class="token class-name class-name-fully-qualified static-context"><span class="token punctuation">\</span>Drupal</span><span class="token operator">::</span><span class="token function">service</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'path_alias.manager'</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">getPathByAlias</span><span class="token punctuation">(</span><span class="token variable">$page_url</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">preg_match</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'/node\/(\d+)/'</span><span class="token punctuation">,</span> <span class="token variable">$node_path</span><span class="token punctuation">,</span> <span class="token variable">$matches</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">      <span class="token variable">$node_id</span> <span class="token operator">=</span> <span class="token function">intval</span><span class="token punctuation">(</span><span class="token variable">$matches</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><mark class="highlight-line highlight-line-active">      <span class="token keyword">if</span> <span class="token punctuation">(</span></mark><br><mark class="highlight-line highlight-line-active">          <span class="token variable">$node_id</span> <span class="token operator">&amp;&amp;</span></mark><br><mark class="highlight-line highlight-line-active">          <span class="token class-name static-context">Node</span><span class="token operator">::</span><span class="token function">load</span><span class="token punctuation">(</span><span class="token variable">$node_id</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token property">field_wrestler_photo</span> <span class="token operator">&amp;&amp;</span> <span class="token comment">// Has wrestler photo field</span></mark><br><mark class="highlight-line highlight-line-active">          <span class="token function">count</span><span class="token punctuation">(</span><span class="token class-name static-context">Node</span><span class="token operator">::</span><span class="token function">load</span><span class="token punctuation">(</span><span class="token variable">$node_id</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token property">field_wrestler_photo</span><span class="token punctuation">)</span> <span class="token comment">// Field has value</span></mark><br><mark class="highlight-line highlight-line-active">      <span class="token punctuation">)</span> <span class="token punctuation">{</span></mark><br><mark class="highlight-line highlight-line-active">        <span class="token variable">$node</span> <span class="token operator">=</span> <span class="token class-name static-context">Node</span><span class="token operator">::</span><span class="token function">load</span><span class="token punctuation">(</span><span class="token variable">$node_id</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Get node object</span></mark><br><mark class="highlight-line highlight-line-active">        <span class="token comment">// ...</span></mark><br><mark class="highlight-line highlight-line-active">      <span class="token punctuation">}</span></mark><br><span class="highlight-line">    <span class="token punctuation">}</span></span><br><span class="highlight-line">  <span class="token punctuation">}</span></span><br><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>You can now access the alt text, and use <code>getFileUri()</code> and the <code>file_url_generator</code> service to get the image’s URL. Finally, assign them to <code>$variable</code> properties so you can reference them in the Twig templates:</p>
<pre class="language-php"><code class="language-php"><span class="highlight-line"><span class="token keyword">function</span> <span class="token function-definition function">swf_preprocess_menu__sub_pages</span><span class="token punctuation">(</span><span class="token operator">&amp;</span><span class="token variable">$variables</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">  <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token variable">$variables</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'items'</span><span class="token punctuation">]</span> <span class="token keyword">as</span> <span class="token variable">$key</span> <span class="token operator">=></span> <span class="token variable">$item</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">    <span class="token variable">$page_url</span> <span class="token operator">=</span> <span class="token variable">$variables</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'items'</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token variable">$key</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'url'</span><span class="token punctuation">]</span><span class="token operator">-></span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">    <span class="token variable">$node_path</span> <span class="token operator">=</span> <span class="token class-name class-name-fully-qualified static-context"><span class="token punctuation">\</span>Drupal</span><span class="token operator">::</span><span class="token function">service</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'path_alias.manager'</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">getPathByAlias</span><span class="token punctuation">(</span><span class="token variable">$page_url</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line">    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">preg_match</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'/node\/(\d+)/'</span><span class="token punctuation">,</span> <span class="token variable">$node_path</span><span class="token punctuation">,</span> <span class="token variable">$matches</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">      <span class="token variable">$node_id</span> <span class="token operator">=</span> <span class="token function">intval</span><span class="token punctuation">(</span><span class="token variable">$matches</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br><span class="highlight-line"></span><br><span class="highlight-line">      <span class="token keyword">if</span> <span class="token punctuation">(</span></span><br><span class="highlight-line">          <span class="token variable">$node_id</span> <span class="token operator">&amp;&amp;</span></span><br><span class="highlight-line">          <span class="token class-name static-context">Node</span><span class="token operator">::</span><span class="token function">load</span><span class="token punctuation">(</span><span class="token variable">$node_id</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token property">field_wrestler_photo</span> <span class="token operator">&amp;&amp;</span></span><br><span class="highlight-line">          <span class="token function">count</span><span class="token punctuation">(</span><span class="token class-name static-context">Node</span><span class="token operator">::</span><span class="token function">load</span><span class="token punctuation">(</span><span class="token variable">$node_id</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token property">field_wrestler_photo</span><span class="token punctuation">)</span></span><br><span class="highlight-line">      <span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br><mark class="highlight-line highlight-line-active">        <span class="token variable">$node</span> <span class="token operator">=</span> <span class="token class-name static-context">Node</span><span class="token operator">::</span><span class="token function">load</span><span class="token punctuation">(</span><span class="token variable">$node_id</span><span class="token punctuation">)</span><span class="token punctuation">;</span></mark><br><mark class="highlight-line highlight-line-active">        <span class="token variable">$image_alt_text</span> <span class="token operator">=</span> <span class="token variable">$node</span><span class="token operator">-></span><span class="token property">field_wrestler_photo</span><span class="token operator">-></span><span class="token property">alt</span><span class="token punctuation">;</span> <span class="token comment">// Get alt text</span></mark><br><mark class="highlight-line highlight-line-active">        <span class="token variable">$file_uri</span> <span class="token operator">=</span> <span class="token variable">$node</span><span class="token operator">-></span><span class="token property">field_wrestler_photo</span><span class="token operator">-></span><span class="token property">entity</span><span class="token operator">-></span><span class="token function">getFileUri</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Get file URI</span></mark><br><mark class="highlight-line highlight-line-active">        <span class="token variable">$image_url</span> <span class="token operator">=</span> <span class="token class-name class-name-fully-qualified static-context"><span class="token punctuation">\</span>Drupal</span><span class="token operator">::</span><span class="token function">service</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'file_url_generator'</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">generateString</span><span class="token punctuation">(</span><span class="token variable">$file_uri</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Get image URL</span></mark><br><mark class="highlight-line highlight-line-active">        <span class="token variable">$variables</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'items'</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token variable">$key</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'image_alt_text'</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token variable">$image_alt_text</span><span class="token punctuation">;</span> <span class="token comment">// Store alt text</span></mark><br><mark class="highlight-line highlight-line-active">        <span class="token variable">$variables</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'items'</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token variable">$key</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'image_url'</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token variable">$image_url</span><span class="token punctuation">;</span> <span class="token comment">// Store image URL</span></mark><br><span class="highlight-line">      <span class="token punctuation">}</span></span><br><span class="highlight-line">    <span class="token punctuation">}</span></span><br><span class="highlight-line">  <span class="token punctuation">}</span></span><br><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>Create a Twig template for the roster lists in <code>templates/navigation/menu--sub_pages.html.twig</code>:</p>
<pre class="language-twig"><code class="language-twig"><span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">import</span> _self as menus <span class="token delimiter punctuation">%}</span></span><br><br><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> menus<span class="token punctuation">.</span>build_menu<span class="token punctuation">(</span>items<span class="token punctuation">,</span> attributes<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">}}</span></span><br><br><span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">macro</span> build_menu<span class="token punctuation">(</span>items<span class="token punctuation">,</span> attributes<span class="token punctuation">,</span> menu_level<span class="token punctuation">)</span> <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">import</span> _self as menus <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">if</span> items <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">for</span> item <span class="token operator">in</span> items <span class="token delimiter punctuation">%}</span></span><br>    <span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> menus<span class="token punctuation">.</span>add_link<span class="token punctuation">(</span>item<span class="token punctuation">,</span> attributes<span class="token punctuation">,</span> menu_level<span class="token punctuation">)</span> <span class="token delimiter punctuation">}}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">endfor</span> <span class="token delimiter punctuation">%}</span></span><br>  <span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">endif</span> <span class="token delimiter punctuation">%}</span></span><br><span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">endmacro</span> <span class="token delimiter punctuation">%}</span></span><br><br><span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">macro</span> add_link<span class="token punctuation">(</span>item<span class="token punctuation">,</span> attributes<span class="token punctuation">,</span> menu_level<span class="token punctuation">)</span> <span class="token delimiter punctuation">%}</span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> item<span class="token punctuation">.</span>url <span class="token delimiter punctuation">}}</span></span><span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>     <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> item<span class="token punctuation">.</span>image_url <span class="token delimiter punctuation">}}</span></span><span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> item<span class="token punctuation">.</span>image_alt_text <span class="token delimiter punctuation">}}</span></span><span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>240<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>360<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token twig language-twig"><span class="token comment">{# Wrestler image #}</span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span><span class="token twig language-twig"><span class="token delimiter punctuation">{{</span> item<span class="token punctuation">.</span>title <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span> <span class="token twig language-twig"><span class="token comment">{# Wrestler name #}</span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><span class="token twig language-twig"><span class="token delimiter punctuation">{%</span> <span class="token tag-name keyword">endmacro</span> <span class="token delimiter punctuation">%}</span></span></code></pre>
<p>You can now create a page for each person in the roster and arrange them into categories by editing the menu struture at <code>/admin/structure/menu/manage/main</code>.</p>
<p>You probably want to restrict the sub-pages block to only appear on the Roster pages by adding relevant paths at <code>/admin/structure/block/manage/sub_pages#edit-visibility-request-path</code>: <code>/roster</code>, <code>roster/men</code>  etc.</p>
<p>We’ve focused here on a custom image field, but you can access any default or custom field in a similar fashion. For example, you could add <code>field_wrestler_height</code> and <code>field_wrestler_weight</code> fields and list each wrestler’s height and weight under their photo.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Finding the perfect domain name</title>
      <link href="https://www.jamieonkeys.dev/posts/perfect-domain-name/"/>
      <updated>2022-10-12T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/perfect-domain-name/</id>
      <content type="html">
        <![CDATA[
      <p><code>.com</code> was once the most coveted of top-level domains (TLDs). For some, that might still be the case. To me, however, much more cool, fun — and often actually available to register<sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup> — are the spate of generic TLDs (gTLDs) that started appearing in the 2010s: <code>.club</code>, <code>.guru</code>, <code>.dev</code>, <code>.buzz</code>, <code>.press</code>, <code>.band</code> and <code>.store</code>, <a href="https://en.wikipedia.org/wiki/List_of_Internet_top-level_domains">among many others</a>.</p>
<p>When coming up with a domain name for your latest venture, the possibilties now seem endless. Even if a <code>.com</code> is still available,<sup class="footnote-ref"><a href="#fn2" id="fnref2">2</a></sup> there’s probably something more eye-catching and a better fit for your project out there.</p>
<p>gTLDs can have their own tone, ranging from the irreverent (<code>.fail</code> and <code>.lol</code>) to the functional (<code>.plumbing</code> and <code>.florist</code>) to the apparently inexplicable (<code>.ooo</code> and <code>.xyz</code>), and they seem to have become somewhat more visiable of late. For example, the advice columnist and podcast host Dan Savage’s domain is now <a href="https://savage.love">savage.love</a>,<sup class="footnote-ref"><a href="#fn3" id="fnref3">3</a></sup> and I’ve just seen <a href="https://freetesting.hiv">freetesting.hiv</a> in an Instagram ad.<sup class="footnote-ref"><a href="#fn4" id="fnref4">4</a></sup> The former GoCompare.com (quite well known in the UK owing to their <a href="https://www.vice.com/en/article/3a8zkb/go-compare-guy-gio-di-compario-interview-2020">annoying</a> TV ads) has gone for a full rebrand as <a href="https://go.compare">Go.Compare</a>, with the <a href="https://press.gocompare.com/news/gocompare-goes-dotty-for-new-name">idea</a> being that ‘searching’ for it on any device will take the user directly to the website rather than an intermediary search engine.</p>
<p>The project I’ve been working on for the past couple of years is a website with a bunch of jazz piano tips and tricks. In the process of thinking about what I should call it, I jotted down some ideas. I thought I’d publish them here as a blog post, along with comments on their suitability. I’m not sure whether the list provides much in the way of insight into how to best go about choosing the ‘perfect’ domain name, but I present it here nonetheless for anyone looking for inspiration.</p>
<p>(These are the domains in the order I jotted them down in my notes file. You can mostly interchange ‘jazz’, ‘jazzkeys’ and ‘jazztoolkit’, as it took some time before I settled on which variant I liked best.)</p>
<ul>
<li><code>jazz.wtf</code><br>
I like this. It’s irreverent. Reflects the state of feeling overwhelmed when learning a complex subject like jazz. Maybe a bit sweary for some people, though.</li>
<li><code>jazz.clinic</code><br>
Nice. It might suggest that the website is some kind of a workshop, which it’s not. Maybe a bit too grand also: I don’t feel in a position to put on a ‘clinic’ in jazz piano. And aren’t clinics more things that drummers do?</li>
<li><code>jazz.tips</code><br>
Like it. Short. Doesn’t suggest something too all-encompassing.</li>
<li><code>jazztoolkit.fyi</code><br>
I like the informality of the ‘.fyi’ domain. Reflects the approach I’m taking. ‘Toolkit’ feels like it reflects what the site will be: a bunch of techniques and approaches to playing jazz piano.</li>
<li><code>jazzpiano.how</code><br>
Cool domain, but <code>.how</code> suggests too grand a scope. The site will be a collection of tips, examples etc.; not a soup-to-nuts course in how to play jazz piano. I’m not sure even the most accomplished jazz piano player would be so arrogant as to think they know the ‘how’: making music is an individual, and often mysterious, thing.</li>
<li><code>jazz.fyi</code><br>
Nice. However, I’m leaning towards focusing on just jazz piano and not jazz in general (though a lot of stuff can also be applied to other instruments). Domain is already registered so I’d need to look into making an offer.</li>
<li><code>jazz.fail</code><br>
Like jazz.wtf, this is irreverent, and I like it; but maybe a bit negative sounding.</li>
<li><code>jazzpiano.tools</code><br>
Goes well with the ‘toolkit’ framing.</li>
<li><code>jazzkeys.tools</code><br>
Fine. I prefer ‘keys’ to ’piano’. It’s more informal, and I’m recording a lot of the examples on an electric piano anyway.</li>
<li><code>jazztoolkit.online</code><br>
OK, I guess. There are better TLDs than <code>.online</code>.</li>
<li><code>jazztoolkit.help</code><br>
Cool TLD that reflects what I want to achieve with the site.</li>
<li><code>jazztoolk.it</code><br>
Too clever. Difficult to verbally convey to others. Not a huge fan of using country-level TLDs for purposes other than those for which they’re intended.</li>
<li><code>jazz.help</code><br>
Fine, but at ~£100 it’s a bit expensive.<sup class="footnote-ref"><a href="#fn5" id="fnref5">5</a></sup></li>
<li><code>jazztoolkit.app</code><br>
I like the <code>.app</code> TLD, and the site does have app-like functionality; but it might confuse non-tech people as it’s a web app, which I don’t think means much to non-tech punters.</li>
<li>🌟 <a href="https://jazzkeys.fyi"><code>jazzkeys.fyi</code></a><br>
OK, this is perfect. Suggests an informal, non-prescriptivist approach: “Here’s what works for me; if it’s of use to you, then great.” Also rolls off the tongue. <a href="https://jazzkeys.fyi">JazzKeys.fyi</a> it is!</li>
</ul>
<blockquote>
<p>JazzKeys.fyi will be launching soon. <a href="https://www.jazzkeys.fyi/#heading-get-notified-on-launch">Sign up to be notified!</a></p>
</blockquote>
<hr class="footnotes-sep">
<section class="footnotes">
<h2 class="h5">Footnotes</h2>
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>A domain registrar that often seems to have good deals is <a href="https://www.namecheap.com">Namecheap</a>. (That’s not an affiliate link, nor is this a sponsored post — I’m just a customer.) <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>You might still want to register the <code>.com</code> if it’s available and add a redirect to your actual domain, in case folk type it out of habit. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>Content may not be suitable office reading if your co-workers are prudes. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>Some other random sites using new TLDs that I’ve encountered: <a href="https://pudding.cool">pudding.cool</a>, <a href="http://bad.coffee">bad.coffee</a>, <a href="https://brr.fyi">brr.fyi</a>, <a href="https://abc.xyz">abc.xyz</a>, <a href="https://jatan.space">jatan.space</a>, <a href="https://dogapi.dog">dogapi.dog</a>, <a href="https://sheep.horse">sheep.horse</a>, <a href="https://mysideproject.rocks">mysideproject.rocks</a> and <a href="https://resumey.pro">resumey.pro</a>. <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p>Someone has subsequently registered <code>jazz.help</code>. <a href="#fnref5" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Using the GitHub REST API to fetch and display gists</title>
      <link href="https://www.jamieonkeys.dev/posts/gists-github-api/"/>
      <updated>2022-09-05T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/gists-github-api/</id>
      <content type="html">
        <![CDATA[
      <p>I wanted to add some additional content to my <a href="/">home page</a>: stuff that didn’t merit a blog post but which I thought was still worth sharing. I’d already been using <a href="https://gist.github.com">GitHub Gist</a> to record code snippets and other notes, so decided to look into using the <a href="https://docs.github.com/en/rest/gists/gists">GitHub REST API</a> to retrieve that data and display it on the page.</p>
<blockquote>
<p><strong>tl;dr:</strong> if you’d rather just dig into the code, and not bother with this blog post, <a href="https://github.com/donbrae/get-gists">there’s a repo on GitHub</a>.</p>
</blockquote>
<p>This post describes a demo version — available at the repo linked above — of the code I deployed on my home page.</p>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#heading-testing-the-api">Testing the API</a></li>
<li><a href="#heading-authenticating">Authenticating</a></li>
<li><a href="#heading-proxy-scripts">Proxy scripts</a>
<ul>
<li><a href="#heading-header.inc"><code>header.inc</code></a></li>
<li><a href="#heading-fetch.php:-fetch-a-listing-of-gists-for-a-user"><code>fetch.php</code>: fetch a listing of gists for a user</a></li>
<li><a href="#heading-fetch-id.php:-fetch-content-of-a-specific-gist"><code>fetch-id.php</code>: fetch content of a specific gist</a></li>
<li><a href="#heading-cors">Cross-Origin Resource Sharing (CORS)</a></li>
<li><a href="#heading-folder-structure">Folder structure</a></li>
</ul>
</li>
<li><a href="#heading-building-the-front-end">Building the front end</a>
<ul>
<li><a href="#heading-installing-a-custom-build-of-highlight.js-as-a-npm-dependency">Installing a custom build of highlight.js as a npm dependency</a></li>
<li><a href="#heading-index.html">index.html</a></li>
<li><a href="#heading-index.js">index.js</a></li>
</ul>
</li>
<li><a href="#heading-summary">Summary</a></li>
</ul>
<h2 id="testing-the-api">Testing the API</h2>
<p>You can query the API to fetch a list of gists for a specific user without authenticating. For example, running this code in your browser console will return a listing of my 15 most recent gists in JSON format (optionally replace <code>donbrae</code> with your own user name):</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'https://api.github.com/users/donbrae/gists?per_page=15'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">response</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><br>  <span class="token comment">// Success</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>response<span class="token punctuation">.</span>ok<span class="token punctuation">)</span><br>    <span class="token keyword">return</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Returns to then()</span><br><br>  <span class="token comment">// Error</span><br>  <span class="token keyword">return</span> Promise<span class="token punctuation">.</span><span class="token function">reject</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">data</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token parameter">err</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Error</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>You can also fetch the actual content of a gist by using the <a href="https://docs.github.com/en/rest/gists/gists#get-a-gist"><code>/gists/{gist_id}</code> endpoint</a>, eg <code>https://api.github.com/gists/d78ee08d2ffdc2f7b8442155f9cf7fa1</code>.</p>
<h2 id="authenticating">Authenticating</h2>
<p>For unauthenticted users, <a href="https://docs.github.com/en/rest/overview/resources-in-the-rest-api#requests-from-personal-accounts">the API is rate-limited to 60 requests per hour</a>. <a href="https://github.com/settings/tokens">Creating a personal access token</a><sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup> and using it to authenticate with the API gets you 5,000 an hour.</p>
<p>When authenticating, we don’t want to expose the access token by making the request directly on the client side, so we can set up proxy scripts on a server to do the authenticating and make the API calls.</p>
<p>I wrote the scripts in PHP because I have some familiarity with the language, and it comes installed with your typical web hosting plan, but you can translate it to your preferred language.</p>
<h2 id="proxy-scripts">Proxy scripts</h2>
<p>The two proxy scripts — one to fetch the list of gists, and one to fetch a particular gist’s content — are in the repo <a href="https://github.com/donbrae/get-gists">donbrae/get-gists</a>.</p>
<h3 id="header.inc"><code>header.inc</code></h3>
<p><code>header.inc</code> contains code we’ll use in both scripts. First we add headers to set the <code>Content-Type</code> to <code>application/json</code> (we’ll be serving JSON), and the <code>Cache-Control</code> header to <code>no-cache</code> (we’ll be implementing our own caching system).</p>
<pre class="language-php"><code class="language-php"><span class="token php language-php"><span class="token delimiter important">&lt;?php</span><br><span class="token function">header</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'Content-Type: application/json'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token function">header</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'Cache-Control: no-cache'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Next, the variable <code>$opts</code> contains our <a href="https://www.php.net/manual/en/context.http.php">HTTP context options</a>, including the request header values we’ll pass to the GitHub API. Replace <code>&lt;token&gt;</code> with your personal access token.</p>
<pre class="language-php"><code class="language-php"><span class="token variable">$opts</span> <span class="token operator">=</span> <span class="token punctuation">[</span><br>  <span class="token string single-quoted-string">'http'</span> <span class="token operator">=></span> <span class="token punctuation">[</span><br>    <span class="token string single-quoted-string">'method'</span> <span class="token operator">=></span> <span class="token string single-quoted-string">'GET'</span><span class="token punctuation">,</span><br>    <span class="token string single-quoted-string">'header'</span> <span class="token operator">=></span> <span class="token punctuation">[</span><br>      <span class="token string single-quoted-string">'User-Agent: PHP'</span><span class="token punctuation">,</span><br>      <span class="token string single-quoted-string">'Content-Type: application/json'</span><span class="token punctuation">,</span><br>      <span class="token string single-quoted-string">'Accept: application/vnd.github+json'</span><span class="token punctuation">,</span><br>      <span class="token string single-quoted-string">'Authorization: token &lt;token>'</span><br>    <span class="token punctuation">]</span><br>  <span class="token punctuation">]</span><br><span class="token punctuation">]</span><span class="token punctuation">;</span></code></pre>
<p>There is also a function that writes files in a way that avoids potential write conflicts:</p>
<pre class="language-php"><code class="language-php"><span class="token keyword">function</span> <span class="token function-definition function">writeFile</span><span class="token punctuation">(</span><span class="token variable">$filename</span><span class="token punctuation">,</span> <span class="token variable">$content</span><span class="token punctuation">,</span> <span class="token variable">$append</span> <span class="token operator">=</span> <span class="token constant boolean">false</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// https://softwareengineering.stackexchange.com/a/332544</span><br>  <span class="token variable">$unique_tmp_filename</span> <span class="token operator">=</span> <span class="token function">uniqid</span><span class="token punctuation">(</span><span class="token string single-quoted-string">''</span><span class="token punctuation">,</span> <span class="token constant boolean">true</span><span class="token punctuation">)</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'.tmp'</span><span class="token punctuation">;</span> <span class="token comment">// Create unique filename</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$append</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token function">file_put_contents</span><span class="token punctuation">(</span><span class="token variable">$unique_tmp_filename</span><span class="token punctuation">,</span> <span class="token function">file_get_contents</span><span class="token punctuation">(</span><span class="token variable">$filename</span><span class="token punctuation">)</span> <span class="token operator">.</span> <span class="token variable">$content</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Concatenate contents of existing file with new content</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    <span class="token function">file_put_contents</span><span class="token punctuation">(</span><span class="token variable">$unique_tmp_filename</span><span class="token punctuation">,</span> <span class="token variable">$content</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token function">rename</span><span class="token punctuation">(</span><span class="token variable">$unique_tmp_filename</span><span class="token punctuation">,</span> <span class="token variable">$filename</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>And a function to parse the response headers:</p>
<pre class="language-php"><code class="language-php"><span class="token keyword">function</span> <span class="token function-definition function">getResponseHeaders</span><span class="token punctuation">(</span><span class="token variable">$http_response</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// https://beamtic.com/parsing-http-response-headers-php</span><br>  <span class="token variable">$response_headers</span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>  <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token variable">$http_response</span> <span class="token keyword">as</span> <span class="token variable">$value</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token constant boolean">false</span> <span class="token operator">!==</span> <span class="token punctuation">(</span><span class="token variable">$matches</span> <span class="token operator">=</span> <span class="token function">explode</span><span class="token punctuation">(</span><span class="token string single-quoted-string">':'</span><span class="token punctuation">,</span> <span class="token variable">$value</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token variable">$response_headers</span><span class="token punctuation">[</span><span class="token string double-quoted-string">"<span class="token interpolation"><span class="token punctuation">{</span><span class="token variable">$matches</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">}</span></span>"</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">trim</span><span class="token punctuation">(</span><span class="token variable">$matches</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br>  <span class="token keyword">return</span> <span class="token variable">$response_headers</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>And, finally, a function to get the HTTP response status code as an integer:</p>
<pre class="language-php"><code class="language-php"><span class="token keyword">function</span> <span class="token function-definition function">getStatus</span><span class="token punctuation">(</span><span class="token variable">$http_response</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// https://stackoverflow.com/a/52662522/4667710</span><br>  <span class="token variable">$status_message</span> <span class="token operator">=</span> <span class="token function">array_shift</span><span class="token punctuation">(</span><span class="token variable">$http_response</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token function">preg_match</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'{HTTP\/\S*\s(\d{3})}'</span><span class="token punctuation">,</span> <span class="token variable">$status_message</span><span class="token punctuation">,</span> <span class="token variable">$match</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token variable">$status</span> <span class="token operator">=</span> <span class="token function">intval</span><span class="token punctuation">(</span><span class="token variable">$match</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">return</span> <span class="token variable">$status</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><span class="token operator">?</span><span class="token operator">></span></code></pre>
<p>(You probably won’t need the last two functions if you’re not using PHP. PHP is weird.)</p>
<h3 id="fetch.php%3A-fetch-a-listing-of-gists-for-a-user"><code>fetch.php</code>: fetch a listing of gists for a user</h3>
<p>Now onto the script that will actually get the data, <code>fetch.php</code>.</p>
<p>We start by including the <code>header.php</code> file:</p>
<pre class="language-php"><code class="language-php"><span class="token php language-php"><span class="token delimiter important">&lt;?php</span><br><span class="token keyword">require_once</span> <span class="token string single-quoted-string">'header.inc'</span><span class="token punctuation">;</span></code></pre>
<p>Next, name a file that we’ll use as a cache:</p>
<pre class="language-php"><code class="language-php"><span class="token variable">$cache_file</span> <span class="token operator">=</span> <span class="token string single-quoted-string">'cached.json'</span><span class="token punctuation">;</span></code></pre>
<p>Now we’ll call the API and store the returned data in variable <code>$content</code>:</p>
<pre class="language-php"><code class="language-php"><span class="token variable">$context</span> <span class="token operator">=</span> <span class="token function">stream_context_create</span><span class="token punctuation">(</span><span class="token variable">$opts</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token variable">$content</span> <span class="token operator">=</span> <span class="token function">file_get_contents</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'https://api.github.com/users/donbrae/gists?per_page=15'</span><span class="token punctuation">,</span> <span class="token constant boolean">false</span><span class="token punctuation">,</span> <span class="token variable">$context</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>We’ll then use a couple of the other functions we declared in <code>header.php</code> to parse out the response status code and headers. This data is available in the <code>$http_response_header</code> array:</p>
<pre class="language-php"><code class="language-php"><span class="token variable">$status</span> <span class="token operator">=</span> <span class="token function">getStatus</span><span class="token punctuation">(</span><span class="token variable">$http_response_header</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token variable">$response_headers</span> <span class="token operator">=</span> <span class="token function">getResponseHeaders</span><span class="token punctuation">(</span><span class="token variable">$http_response_header</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>If the status is <code>200</code>, we output the JSON returned from the API:</p>
<pre class="language-php"><code class="language-php"><span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$status</span> <span class="token operator">===</span> <span class="token number">200</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">echo</span> <span class="token variable">$content</span><span class="token punctuation">;</span></code></pre>
<p>Next we add a conditional that checks the <code>X-RateLimit-Remaining</code> response header to determine whether we should write a copy of the returned JSON to our cache file:</p>
<pre class="language-php"><code class="language-php">  <span class="token keyword">if</span> <span class="token punctuation">(</span><br>    <span class="token function">intval</span><span class="token punctuation">(</span><span class="token variable">$response_headers</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'X-RateLimit-Remaining'</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">&lt;</span> <span class="token number">250</span> <span class="token operator">&amp;&amp;</span> <span class="token comment">// We may hit the API rate limit soon</span><br>    <span class="token function">file_exists</span><span class="token punctuation">(</span><span class="token variable">$cache_file</span><span class="token punctuation">)</span> <span class="token operator">&amp;&amp;</span> <span class="token punctuation">(</span><span class="token function">time</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token function">filemtime</span><span class="token punctuation">(</span><span class="token variable">$cache_file</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">/</span> <span class="token number">60</span> <span class="token operator">/</span> <span class="token number">60</span> <span class="token operator">></span> <span class="token number">6</span> <span class="token operator">||</span> <span class="token comment">// Cached file hasn’t been updated in last 6 hours</span><br>    <span class="token operator">!</span><span class="token function">file_exists</span><span class="token punctuation">(</span><span class="token variable">$cache_file</span><span class="token punctuation">)</span> <span class="token comment">// Or there is no cache file</span><br>  <span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token function">writeFile</span><span class="token punctuation">(</span><span class="token variable">$cache_file</span><span class="token punctuation">,</span> <span class="token variable">$content</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Write a cached version of JSON in case API rate limit is reached</span><br>  <span class="token punctuation">}</span></code></pre>
<p>I set the threshold to an arbitrary 250 API calls remaning. The conditional could instead in theory be <code>X-RateLimit-Remaining === 0</code>, but the script that fetches the actual gist content (<code>fetch-id.php</code>) also counts towards API usage, so we can’t guarantee that <code>fetch.php</code> will be called when the rate limit is exactly zero.<sup class="footnote-ref"><a href="#fn2" id="fnref2">2</a></sup></p>
<p>There is also a condition which checks that the cache file hasn’t been updated in the last six hours, to avoid continually writing a new cache file every time this script is called and <code>X-RateLimit-Remaining</code> is under 250.</p>
<p>Alternatively, if no cache file exists — regardless of other conditions — write one.</p>
<p>If another status is returned, and if there is a cached version, serve the cached version; otherwise, return an error message in JSON format:</p>
<pre class="language-php"><code class="language-php"><span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">file_exists</span><span class="token punctuation">(</span><span class="token variable">$cache_file</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Status ?304, 403 or 422</span><br>  <span class="token keyword">echo</span> <span class="token function">file_get_contents</span><span class="token punctuation">(</span><span class="token string double-quoted-string">"cached.json"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Serve cached copy</span><br><span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>  <span class="token keyword">echo</span> <span class="token string double-quoted-string">"{\"error\": \"Cannot fetch list of gists. Error code: <span class="token interpolation"><span class="token variable">$status</span></span>\"}"</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><span class="token operator">?</span><span class="token operator">></span></code></pre>
<h3 id="cors">CORS</h3>
<p>Add <code>fetch.php</code> to your server in a folder called <code>gists</code>. If you’re using another domain to host the proxy scripts — as I’m doing because my blog is a static site hosted on Netlify, which doesn’t support PHP — we’ll need to make sure <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">Cross-Origin Resource Sharing (CORS)</a> is set up properly. We need to tell our server hosting <code>fetch.php</code> to accept Ajax requests from our originating domain. In my case, the proxy domain — donbrae.co.uk — needs to accept requests from browsers viewing jamieonkeys.dev.</p>
<p>To do this — and assuming your server runs Apache — add a <code>.htaccess</code> file in the <code>gists</code> folder on donbrae.co.uk:</p>
<pre class="language-shell"><code class="language-shell"><span class="token operator">&lt;</span>IfModule mod_headers.c<span class="token operator">></span><br>  Header <span class="token builtin class-name">set</span> Access-Control-Allow-Origin: https://www.jamieonkeys.dev<br><span class="token operator">&lt;</span>/IfModule<span class="token operator">></span><br></code></pre>
<figure>
  <!-- https://excalidraw.com/#json=TLZyF_DF26zZt3aW7L_hN,LVY7UBc0H9-MKCTSTKc0VQ -->
  <img src="/img/github-api-proxy.png" class="nae-shadow img-transparent" width="714" height="484" alt="Diagram showing a client browser viewing the website jamieonkeys.dev. The website requests data from a server at donbrae.co.uk, which — acting as a proxy — fetches data from the GitHub API, which it then returns it to the client. The donbrae.co.uk server has a CORS policy which allows client requests to be made from jamieonkeys.dev.">
  <figcaption>Add a CORS directive to proxy server donbrae&period;co&period;uk to allow the website jamieonkeys.dev to fetch data (which is in turn pulled from the GitHub API) from it.</figcaption>
</figure>
<p>The website at jamieonkeys.dev should now be able to call <code>https://donbrae.co.uk/proxy-php/gists/fetch.php</code> and get the data in return.</p>
<h3 id="fetch-id.php%3A-fetch-content-of-a-specific-gist"><code>fetch-id.php</code>: fetch content of a specific gist</h3>
<p>There is also a proxy script, <code>fetch-id.php</code>, to get the content of specific gists.</p>
<p>Again, we start by including the same <code>header.inc</code> file we used previously:</p>
<pre class="language-php"><code class="language-php"><span class="token php language-php"><span class="token delimiter important">&lt;?php</span><br><span class="token keyword">require_once</span> <span class="token string single-quoted-string">'header.inc'</span><span class="token punctuation">;</span></code></pre>
<p>On the front end we’ll be passing a gist ID in the querystring parameter <code>gist_id</code>, so capture that:</p>
<pre class="language-php"><code class="language-php"><span class="token variable">$gist_id</span> <span class="token operator">=</span> <span class="token variable">$_GET</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'gist_id'</span><span class="token punctuation">]</span><span class="token punctuation">;</span></code></pre>
<p>The GitHub API returns a status code of <code>304</code> for gists which haven’t been updated since a specified date. Requests which result in a <code>304</code> also don’t count towards your rate limit. We’ll create a caching system to return static JSON files from our server so we don’t need to keep calling the API.</p>
<p>Add a subfolder called on your server under <code>gists</code> called <code>gistscache</code>.</p>
<p><code>fetch-id.php</code> continues:</p>
<pre class="language-php"><code class="language-php"><span class="token variable">$cached_file_path</span> <span class="token operator">=</span> <span class="token string double-quoted-string">"./gistscache/<span class="token interpolation"><span class="token variable">$gist_id</span></span>.json"</span><span class="token punctuation">;</span><br><span class="token variable">$cached_file_exists</span> <span class="token operator">=</span> <span class="token function">file_exists</span><span class="token punctuation">(</span><span class="token variable">$cached_file_path</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$cached_file_exists</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// Get last updated date of file in GMT</span><br>  <span class="token variable">$last_modified</span> <span class="token operator">=</span> <span class="token function">filemtime</span><span class="token punctuation">(</span><span class="token variable">$cached_file_path</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Get its last modified date</span><br>  <span class="token variable">$last_modified_gmt</span> <span class="token operator">=</span> <span class="token function">gmdate</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'D, d M Y H:i:s'</span><span class="token punctuation">,</span> <span class="token variable">$last_modified</span><span class="token punctuation">)</span> <span class="token operator">.</span> <span class="token string single-quoted-string">' GMT'</span><span class="token punctuation">;</span> <span class="token comment">// Format it so GitHub API accepts it in header</span><br><br>  <span class="token comment">// Add date to header so that API will return 304 if no updates have been made, and we can serve the cached copy; otherwise we can expect a 200 with the latest data</span><br>  <span class="token function">array_push</span><span class="token punctuation">(</span><span class="token variable">$opts</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'http'</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'header'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token string double-quoted-string">"If-Modified-Since: <span class="token interpolation"><span class="token variable">$last_modified_gmt</span></span>"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p><code>fetch-id.php</code> always checks for a cached copy of the requested gist in the <code>gistscache</code> folder, and if one exists, gets the file’s last modified date and adds it to the headers we’ll pass to the GitHub API.</p>
<p>If the gist has been updated since that date, a status of <code>200</code> will be returned along with the latest data. If the gist hasn’t been updated, a <code>304</code> will be returned and we’ll serve the cached data.</p>
<p>Next we make the API call and store the status code:</p>
<pre class="language-php"><code class="language-php"><span class="token variable">$context</span> <span class="token operator">=</span> <span class="token function">stream_context_create</span><span class="token punctuation">(</span><span class="token variable">$opts</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token variable">$content</span> <span class="token operator">=</span> <span class="token function">file_get_contents</span><span class="token punctuation">(</span><span class="token string double-quoted-string">"https://api.github.com/gists/<span class="token interpolation"><span class="token variable">$gist_id</span></span>"</span><span class="token punctuation">,</span> <span class="token constant boolean">false</span><span class="token punctuation">,</span> <span class="token variable">$context</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token variable">$status</span> <span class="token operator">=</span> <span class="token function">getStatus</span><span class="token punctuation">(</span><span class="token variable">$http_response_header</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Add then we handle the API response:</p>
<pre class="language-php"><code class="language-php"><span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$status</span> <span class="token operator">===</span> <span class="token number">200</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">echo</span> <span class="token variable">$content</span><span class="token punctuation">;</span> <span class="token comment">// Serve JSON returned from API</span><br>  <span class="token function">writeFile</span><span class="token punctuation">(</span><span class="token variable">$cached_file_path</span><span class="token punctuation">,</span> <span class="token variable">$content</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Add/update cached copy</span><br><span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><br>  <span class="token variable">$status</span> <span class="token operator">===</span> <span class="token number">304</span> <span class="token operator">&amp;&amp;</span> <span class="token variable">$cached_file_exists</span> <span class="token operator">||</span> <span class="token comment">// Not modified and we have a cached version</span><br>  <span class="token variable">$status</span> <span class="token operator">===</span> <span class="token number">403</span> <span class="token operator">&amp;&amp;</span> <span class="token variable">$cached_file_exists</span> <span class="token comment">// We've likely reached our API limit (403 == Forbidden Gist)</span><br><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">echo</span> <span class="token function">file_get_contents</span><span class="token punctuation">(</span><span class="token variable">$cached_file_path</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Serve cached copy</span><br><span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>  <span class="token keyword">echo</span> <span class="token string double-quoted-string">"{\"error\": \"Cannot fetch gist. Error code: <span class="token interpolation"><span class="token variable">$status</span></span>\"}"</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>If we get a <code>200</code> — meaning the gist has been updated since the last updated date we sent in the request headers — serve the returned JSON and write a copy to the cache. If we get <code>304</code> — meaning the gist hasn’t been updated — serve a cached copy. If we get a <code>403</code>, it means we’ve reached our API limit, so also serve the cached copy. In any other case, serve an error message in JSON format.</p>
<h3 id="folder-structure">Folder structure</h3>
<p>In summary, the <code>gists</code> folder on your proxy server should look something like this:</p>
<pre class="language-text"><code class="language-text">.<br>├── .htaccess<br>├── cached.json<br>├── fetch-id.php<br>├── fetch.php<br>├── gistscache<br>│   ├── 0276d43b831af40d1bbe529549a66f84.json<br>│   ├── 2277dd0362789957fd5ce9ed4894c93b.json<br>│   ├── 35841f68de35bce70ea1bb4cd71ac5d1.json<br>│   └── [etc.]<br>└── headers.inc</code></pre>
<h2 id="building-the-front-end">Building the front end</h2>
<p>If you’ve not done so already, grab the code for the front end by cloning <a href="https://github.com/donbrae/get-gists">get-gists</a> on your local machine:</p>
<pre class="language-shell"><code class="language-shell"><span class="token function">git</span> clone https://github.com/donbrae/get-gists.git</code></pre>
<p><code>cd</code> into the <code>get-gists</code> folder and run <code>npm install</code>.</p>
<p>Next, start a local server with <code>npm run start</code>. The page should open in your default browser.</p>
<p>Let’s dig into the code.</p>
<h3 id="installing-a-custom-build-of-highlight.js-as-a-npm-dependency">Installing a custom build of <code>highlight.js</code> as a npm dependency</h3>
<p>We use <a href="https://www.npmjs.com/package/highlight.js"><code>highlight.js</code></a> to add syntax highlighting to gists that contain code. By default <code>highlight.js</code> is over 1 MB in size, so to minimise the amount of JavaScript we serve to the user, I created a custom build with only the languages I need, namely JavaScript, HTML, CSS, PHP and Markdown.</p>
<p>You’d normally install <code>highlight.js</code> from the public npm registry via <code>npm install highlight.js</code>, but I instead followed these steps:</p>
<ol>
<li><code>cd</code> to your development folder and clone the <code>highlight.js</code> repo: <code>git clone https://github.com/highlightjs/highlight.js.git</code></li>
<li><code>cd highlight.js</code></li>
<li><code>npm install</code></li>
<li>Run a build for just the languages we need: <code>node tools/build.js javascript xml css php markdown</code> (<code>xml</code> includes HTML)</li>
<li>The files we create are in <code>./build</code>. Run <code>ls -l ./build</code> to list them</li>
<li><code>cd</code> to the root of the project folder you want to use the custom build with (in my case, <code>get-gists</code>)</li>
<li><code>mkdir src</code> to create a <code>src</code> folder</li>
<li><code>mkdir src/highlight.js</code> to create a folder for the <code>highlight.js</code> custom build</li>
<li><code>cd src/highlight.js</code></li>
<li>Use <code>cp</code> to copy over the custom build we created in step 4, eg <code>cp ../../../highlight.js/build/highlight.js ./</code></li>
<li>Run <code>npm init</code> to set the folder up as an npm dependency</li>
<li>Answer the prompts. Call it <code>highlight.js</code> and make sure <code>entry point</code> is set to <code>highlight.js</code> (it should be auto-selected)</li>
<li><code>cd ../..</code> to root of your project folder</li>
<li>Run <code>npm install ./src/highlight.js</code></li>
<li>You should now see <code>highlight.js</code> referenced as a dependency in your project’s main <code>package.json</code>: <code>grep --color=always -e &quot;^&quot; -e &quot;highlight.js&quot; package.json</code></li>
<li><code>highlight.js</code> can now be imported from within a JavaScript file in your project: <code>import hljs from &quot;highlight.js&quot;;</code></li>
</ol>
<h3 id="index.html"><code>index.html</code></h3>
<p>You’ll see in the repo that <code>index.html</code> is a basic HTML page with a button — which, when clicked, will initiate a <code>fetch()</code> of the gist data — and an empty <code>&lt;div&gt;</code> where we’ll add the returned data. We also include the main script, <code>index.js</code>:</p>
<pre class="language-html"><code class="language-html"><span class="token doctype"><span class="token punctuation">&lt;!</span><span class="token doctype-tag">DOCTYPE</span> <span class="token name">html</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>Get gists<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">charset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>UTF-8<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span><span class="token punctuation">></span></span>Get gists demo<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>get-gists<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Get gists<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>gists<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hide fade<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>src/index.js<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span></code></pre>
<h3 id="index.js"><code>index.js</code></h3>
<p>Note that, here, the two <code>fetch</code> calls we make call the GitHub API directly, but in production these can be swapped for the <a href="#heading-proxy-scripts">proxy URLs</a>.</p>
<p>First in <code>index.js</code> we import our CSS files and JavaScript modules:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token string">'./styles.css'</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token string">'./xcode.css'</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> hljs <span class="token keyword">from</span> <span class="token string">'highlight.js'</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> markdownit <span class="token keyword">from</span> <span class="token string">'markdown-it'</span><span class="token punctuation">;</span></code></pre>
<ul>
<li><code>styles.css</code> is some basic CSS for styling the page that lists the gists</li>
<li><code>xcode.css</code> is a set of CSS classes that style the code to look like Apple’s <a href="https://developer.apple.com/xcode/">Xcode IDE</a></li>
<li><code>highlight.js</code> is our custom build of the code-highlightling module</li>
<li><code>markdown-it</code> transforms gists written in Markdown to HTML</li>
</ul>
<p>Next we define a couple of functions. For security reasons, <code>escapeHtml()</code> sanitises any HTML that is returned by the API, replacing certain characters with their <a href="https://developer.mozilla.org/en-US/docs/Glossary/Entity">HTML entity</a> equivalents:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token comment">// https://stackoverflow.com/a/6234804</span><br><span class="token keyword">function</span> <span class="token function">escapeHtml</span><span class="token punctuation">(</span><span class="token parameter">unsafe</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> unsafe<br>    <span class="token punctuation">.</span><span class="token function">replaceAll</span><span class="token punctuation">(</span><span class="token string">'&amp;'</span><span class="token punctuation">,</span> <span class="token string">'&amp;amp;'</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">replaceAll</span><span class="token punctuation">(</span><span class="token string">'&lt;'</span><span class="token punctuation">,</span> <span class="token string">'&amp;lt;'</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">replaceAll</span><span class="token punctuation">(</span><span class="token string">'>'</span><span class="token punctuation">,</span> <span class="token string">'&amp;gt;'</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">replaceAll</span><span class="token punctuation">(</span><span class="token string">'"'</span><span class="token punctuation">,</span> <span class="token string">'&amp;quot;'</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">replaceAll</span><span class="token punctuation">(</span><span class="token string">"'"</span><span class="token punctuation">,</span> <span class="token string">'&amp;#039;'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p><code>show()</code> handles element fade-ins:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">show</span><span class="token punctuation">(</span><span class="token parameter">el</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  el<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token string">'hide'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    el<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">'show'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">30</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>Next, the <code>cfg</code> object defines various properties, such as the GitHub user account we’re fetching gists for, and the IDs of any gists we wish to exclude from our listing:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> cfg <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">githubUser</span><span class="token operator">:</span> <span class="token string">'donbrae'</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">hideIds</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>    <span class="token comment">// IDs of Gists to exclude from page</span><br>    <span class="token string">'2369abb83a0f3d53fbc3aba963e80f7c'</span><span class="token punctuation">,</span> <span class="token comment">// PDF page numbers</span><br>    <span class="token string">'bfbda44e3bb5c2883a25acc5a759c8fc'</span><span class="token punctuation">,</span> <span class="token comment">// Bootstrap 5 colour gradient</span><br>    <span class="token string">'ab4e15be962602b1bf4975b912b14939'</span> <span class="token comment">// Apple Music shortcuts</span><br>  <span class="token punctuation">]</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">perPage</span><span class="token operator">:</span> <span class="token number">15</span><span class="token punctuation">,</span> <span class="token comment">// Number of gists to fetch from API</span><br>  <span class="token literal-property property">gistsLimit</span><span class="token operator">:</span> <span class="token number">10</span> <span class="token comment">// Maximum number of gists to add to page</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>So that we can display the name of the month next to each gist, we define an array of month name abbreviations:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> months <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">'Jan'</span><span class="token punctuation">,</span> <span class="token string">'Feb'</span><span class="token punctuation">,</span> <span class="token string">'Mar'</span><span class="token punctuation">,</span> <span class="token string">'Apr'</span><span class="token punctuation">,</span> <span class="token string">'May'</span><span class="token punctuation">,</span> <span class="token string">'Jun'</span><span class="token punctuation">,</span> <span class="token string">'Jul'</span><span class="token punctuation">,</span> <span class="token string">'Aug'</span><span class="token punctuation">,</span> <span class="token string">'Sep'</span><span class="token punctuation">,</span> <span class="token string">'Oct'</span><span class="token punctuation">,</span> <span class="token string">'Nov'</span><span class="token punctuation">,</span> <span class="token string">'Dec'</span><span class="token punctuation">]</span><span class="token punctuation">;</span></code></pre>
<p>Next, we have a regular expression to parse text for URLs so we can turn them into links:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> urlRegEx <span class="token operator">=</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">(\b(https):\/\/[-A-Z0-9+&amp;@#%?=~_|!:,.;]*[-A-Z0-9+&amp;@#%=~_|])</span><span class="token regex-delimiter">/</span><span class="token regex-flags">gi</span></span><span class="token punctuation">;</span> <span class="token comment">// Only transform https URLs. Source: https://www.codespeedy.com/replace-url-with-clickable-link-javascript/</span></code></pre>
<p>We also grab the HTML element into which we’ll place the returned gists:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> gists <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'gists'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>And then add an event listener to the ‘Get gists’ button:</p>
<pre class="language-javascript"><code class="language-javascript">document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'get-gists'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> getGists<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>This calls our main function, <code>getGists()</code>:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">getGists</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> btnGetGists <span class="token operator">=</span> e<span class="token punctuation">.</span>target<span class="token punctuation">;</span><br>  btnGetGists<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">'fade'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Fade out button</span><br><br>  <span class="token keyword">function</span> <span class="token function">error</span><span class="token punctuation">(</span><span class="token parameter">err<span class="token punctuation">,</span> container<span class="token punctuation">,</span> e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">const</span> escapedHTML <span class="token operator">=</span> <span class="token function">escapeHtml</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>err<span class="token punctuation">.</span>status<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>err<span class="token punctuation">.</span>statusText<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>err<span class="token punctuation">.</span>url<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    container<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;div></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>err<span class="token punctuation">.</span>status<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>err<span class="token punctuation">.</span>statusText<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>err<span class="token punctuation">.</span>url<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/div></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>    <span class="token function">show</span><span class="token punctuation">(</span>container<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">const</span> button <span class="token operator">=</span> e<span class="token punctuation">.</span>target<span class="token punctuation">;</span><br>    button<span class="token punctuation">.</span>parentNode<span class="token punctuation">.</span><span class="token function">removeChild</span><span class="token punctuation">(</span>button<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token function">fetch</span><span class="token punctuation">(</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://api.github.com/users/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>cfg<span class="token punctuation">.</span>githubUser<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/gists?per_page=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>cfg<span class="token punctuation">.</span>perPage<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><br>  <span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token keyword">if</span> <span class="token punctuation">(</span>response<span class="token punctuation">.</span>ok<span class="token punctuation">)</span> <span class="token keyword">return</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>      <span class="token keyword">return</span> Promise<span class="token punctuation">.</span><span class="token function">reject</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><br>      <span class="token keyword">if</span> <span class="token punctuation">(</span>data<span class="token punctuation">.</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>data<span class="token punctuation">.</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token keyword">return</span><span class="token punctuation">;</span><br>      <span class="token punctuation">}</span><br><br>      <span class="token comment">/**<br>       * Pseudocode block 1: show listing of gists returned by API<br>       */</span><br>      <span class="token comment">// Filter returned items to remove specific gists by ID</span><br>      <span class="token comment">// Loop through filtered gists</span><br>      <span class="token comment">//   Get created date</span><br>      <span class="token comment">//   Get description</span><br>      <span class="token comment">//     Convert any URLs within description to links</span><br>      <span class="token comment">//     Transform backticked text to &lt;code> elements</span><br>      <span class="token comment">//   Create &lt;div> for gist with date, description and 'Get gist' button</span><br>      <span class="token comment">//     Add to button the gist ID as a `data` attribute</span><br>      <span class="token comment">// End loop</span><br>      <span class="token comment">// Add gists to DOM</span><br>      <span class="token comment">// Fade in gists</span><br><br>      <span class="token comment">/**<br>       * Pseudocode block 2: show specific gist's content<br>       */</span><br>      <span class="token comment">// Loop through 'Get gist' buttons in DOM</span><br>      <span class="token comment">//   Add on-click function</span><br>      <span class="token comment">//     Fetch 'https://api.github.com/gists/&lt;gist-id>'</span><br>      <span class="token comment">//       Get gist type</span><br>      <span class="token comment">//         If type is text/html</span><br>      <span class="token comment">//           Escape the HTML</span><br>      <span class="token comment">//         If type is text/markdown</span><br>      <span class="token comment">//           Run the content through markdownit</span><br>      <span class="token comment">//       Add to DOM</span><br>      <span class="token comment">//         If text/markdown</span><br>      <span class="token comment">//           Add gist content in a &lt;div></span><br>      <span class="token comment">//         Else</span><br>      <span class="token comment">//           Add to UI in a code block</span><br>      <span class="token comment">//           Run code block through highlight.js</span><br>      <span class="token comment">//   End on-click function</span><br>      <span class="token comment">// End loop</span><br>      <span class="token comment">// Fade in gist</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">err</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token function">error</span><span class="token punctuation">(</span>err<span class="token punctuation">,</span> gists<span class="token punctuation">,</span> e<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>The <code>fetch()</code> function is the same in structure as the <a href="#heading-testing-the-api">one we tested at the start of this post</a>.</p>
<p>You’ll see a couple of blocks of pseudocode which run when the API call is successful. I find that writing such a high-level overview of the various steps in a process helps me think through a problem without getting bogged down in specifics. Now that we’ve outlined the main steps in pseudocode, we can convert them to real code:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token comment">// [...]</span><br>      <span class="token comment">/**<br>       * Show listing of gists returned by API<br>       */</span><br>      <span class="token keyword">const</span> items <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><br>      <span class="token comment">// Filter returned items to remove specific gists by ID</span><br>      <span class="token keyword">const</span> dataFiltered <span class="token operator">=</span> data<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><br>        <span class="token punctuation">(</span><span class="token parameter">gist</span><span class="token punctuation">)</span> <span class="token operator">=></span> cfg<span class="token punctuation">.</span>hideIds<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span>gist<span class="token punctuation">.</span>id<span class="token punctuation">)</span> <span class="token operator">===</span> <span class="token operator">-</span><span class="token number">1</span><br>      <span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>      <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> cfg<span class="token punctuation">.</span>gistsLimit<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Loop through filtered gists</span><br>        <span class="token keyword">if</span> <span class="token punctuation">(</span>dataFiltered<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><br>          <span class="token comment">// Get created date</span><br>          <span class="token keyword">const</span> date <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span>dataFiltered<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span>created_at<span class="token punctuation">)</span><span class="token punctuation">;</span><br>          <span class="token keyword">const</span> dateFormatted <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>date<span class="token punctuation">.</span><span class="token function">getDate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><br>            months<span class="token punctuation">[</span>date<span class="token punctuation">.</span><span class="token function">getMonth</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">]</span><br>          <span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>          <span class="token keyword">const</span> verb <span class="token operator">=</span> <span class="token operator">!</span>i <span class="token operator">?</span> <span class="token string">'Created '</span> <span class="token operator">:</span> <span class="token string">''</span><span class="token punctuation">;</span><br>          <span class="token keyword">const</span> year <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> ’</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>date<span class="token punctuation">.</span><span class="token function">getFullYear</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>          <span class="token comment">// Get description</span><br>          <span class="token keyword">let</span> description <span class="token operator">=</span> dataFiltered<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span>description<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>length<br>            <span class="token operator">?</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;div></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>dataFiltered<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span>description<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/div></span><span class="token template-punctuation string">`</span></span><br>            <span class="token operator">:</span> <span class="token string">''</span><span class="token punctuation">;</span><br><br>          <span class="token comment">// Convert any URLs within description to links</span><br>          description <span class="token operator">=</span> description<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span>urlRegEx<span class="token punctuation">,</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">match</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>            <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;a href="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>match<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>match<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/a></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>          <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>          <span class="token comment">// Transform backticked text to &lt;code> elements</span><br>          description <span class="token operator">=</span> description<span class="token punctuation">.</span><span class="token function">replaceAll</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">`(.+?)`</span><span class="token regex-delimiter">/</span><span class="token regex-flags">gi</span></span><span class="token punctuation">,</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">match</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>            <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;code></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>match<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/code></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>          <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>          <span class="token comment">// Create &lt;div> for gist with date, description and 'Get gist' button</span><br>          items<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><br>            <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;div class="gist-container"><br>              &lt;h2>&lt;a href="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><br>                dataFiltered<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span>html_url<br>              <span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><br>              Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>dataFiltered<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span>files<span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><br>            <span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/a>&lt;/h2> &lt;span class="dt-published"></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>verb<span class="token interpolation-punctuation punctuation">}</span></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>dateFormatted<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>year<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/span></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>description<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br>              &lt;button class="get-gist display-block button button-sm mt-1" data-gist-id="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><br>                dataFiltered<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span>id <span class="token comment">// Add to button the gist ID as a `data` attribute</span><br>              <span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">">Show&lt;/button><br>              &lt;div id="gist-</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>dataFiltered<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" class="gist-content hide fade">&lt;/div><br>            &lt;/div></span><span class="token template-punctuation string">`</span></span><br>          <span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">break</span><span class="token punctuation">;</span><br>      <span class="token punctuation">}</span><br><br>      gists<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> items<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Add gists to DOM</span><br>      btnGetGists<span class="token punctuation">.</span>parentNode<span class="token punctuation">.</span><span class="token function">removeChild</span><span class="token punctuation">(</span>btnGetGists<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Remove 'Get gists' button</span><br>      <span class="token function">show</span><span class="token punctuation">(</span>gists<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Fade in gists</span><br><br>      <span class="token comment">/**<br>       * Show specific gist's content<br>       */</span><br><br>      <span class="token comment">// Loop through 'Get gist' buttons in DOM</span><br>      <span class="token keyword">const</span> getGists <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'.get-gist'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      getGists<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">getGistButton</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><br>        <span class="token comment">// Add on-click function</span><br>        getGistButton<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>          <span class="token keyword">const</span> gistId <span class="token operator">=</span> e<span class="token punctuation">.</span>target<span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>gistId<span class="token punctuation">;</span><br>          <span class="token keyword">const</span> btn <span class="token operator">=</span> e<span class="token punctuation">.</span>target<span class="token punctuation">;</span><br>          btn<span class="token punctuation">.</span>disabled <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token comment">// Make sure user only triggers one API call</span><br>          btn<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">'fade'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>          <span class="token keyword">const</span> el <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">gist-</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>gistId<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>          <span class="token comment">// Fetch 'https://api.github.com/gists/&lt;gist-id>'</span><br>          <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://api.github.com/gists/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>gistId<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br>            <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>              <span class="token keyword">if</span> <span class="token punctuation">(</span>response<span class="token punctuation">.</span>ok<span class="token punctuation">)</span> <span class="token keyword">return</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>              <span class="token keyword">return</span> Promise<span class="token punctuation">.</span><span class="token function">reject</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">;</span><br>            <span class="token punctuation">}</span><span class="token punctuation">)</span><br>            <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><br>              <span class="token keyword">if</span> <span class="token punctuation">(</span>data<span class="token punctuation">.</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>                console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>data<span class="token punctuation">.</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span><br>                el<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;div class="gist-content"></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">escapeHtml</span><span class="token punctuation">(</span>data<span class="token punctuation">.</span>error<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/div></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>                btn<span class="token punctuation">.</span>parentNode<span class="token punctuation">.</span><span class="token function">removeChild</span><span class="token punctuation">(</span>btn<span class="token punctuation">)</span><span class="token punctuation">;</span><br>                <span class="token function">show</span><span class="token punctuation">(</span>el<span class="token punctuation">)</span><span class="token punctuation">;</span><br>                <span class="token keyword">return</span><span class="token punctuation">;</span><br>              <span class="token punctuation">}</span><br><br>              <span class="token keyword">const</span> gistName <span class="token operator">=</span> Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>data<span class="token punctuation">.</span>files<span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>              <span class="token keyword">const</span> gist <span class="token operator">=</span> data<span class="token punctuation">.</span>files<span class="token punctuation">[</span>gistName<span class="token punctuation">]</span><span class="token punctuation">;</span><br>              <span class="token keyword">let</span> gistContent<span class="token punctuation">;</span><br><br>              <span class="token comment">// Get gist type</span><br>              <span class="token keyword">if</span> <span class="token punctuation">(</span>gist<span class="token punctuation">.</span>type <span class="token operator">===</span> <span class="token string">'text/html'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// If type is text/html</span><br>                gistContent <span class="token operator">=</span> <span class="token function">escapeHtml</span><span class="token punctuation">(</span>gist<span class="token punctuation">.</span>content<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Escape the HTML</span><br>              <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>gist<span class="token punctuation">.</span>type <span class="token operator">===</span> <span class="token string">'text/markdown'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// If type is text/markdown</span><br>                <span class="token keyword">const</span> md <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">markdownit</span><span class="token punctuation">(</span><span class="token string">'default'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">html</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Run the content through markdownit</span><br>                gistContent <span class="token operator">=</span> md<span class="token punctuation">.</span><span class="token function">render</span><span class="token punctuation">(</span>gist<span class="token punctuation">.</span>content<span class="token punctuation">)</span><span class="token punctuation">;</span><br>              <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>                gistContent <span class="token operator">=</span> gist<span class="token punctuation">.</span>content<span class="token punctuation">;</span><br>              <span class="token punctuation">}</span><br><br>              <span class="token comment">// Add to DOM</span><br>              <span class="token keyword">if</span> <span class="token punctuation">(</span>gist<span class="token punctuation">.</span>type <span class="token operator">===</span> <span class="token string">'text/markdown'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// If text/markdown</span><br>                el<span class="token punctuation">.</span><span class="token function">insertAdjacentHTML</span><span class="token punctuation">(</span> <span class="token comment">// Add gist content in a &lt;div></span><br>                  <span class="token string">'beforeend'</span><span class="token punctuation">,</span><br>                  <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;div></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>gistContent<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/div></span><span class="token template-punctuation string">`</span></span><br>                <span class="token punctuation">)</span><span class="token punctuation">;</span><br>              <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>                el<span class="token punctuation">.</span><span class="token function">insertAdjacentHTML</span><span class="token punctuation">(</span> <span class="token comment">// Add to UI in a code block</span><br>                  <span class="token string">'beforeend'</span><span class="token punctuation">,</span><br>                  <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;pre class="code" role="code"></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>gistContent<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/pre></span><span class="token template-punctuation string">`</span></span><br>                <span class="token punctuation">)</span><span class="token punctuation">;</span><br>                hljs<span class="token punctuation">.</span><span class="token function">highlightElement</span><span class="token punctuation">(</span>el<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'pre'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Run code block through highlight.js</span><br>              <span class="token punctuation">}</span><br><br>              btn<span class="token punctuation">.</span>parentNode<span class="token punctuation">.</span><span class="token function">removeChild</span><span class="token punctuation">(</span>btn<span class="token punctuation">)</span><span class="token punctuation">;</span><br>              <span class="token function">show</span><span class="token punctuation">(</span>el<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Fade in gist</span><br>            <span class="token punctuation">}</span><span class="token punctuation">)</span><br>            <span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">err</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>              <span class="token function">error</span><span class="token punctuation">(</span>err<span class="token punctuation">,</span> el<span class="token punctuation">,</span> e<span class="token punctuation">)</span><span class="token punctuation">;</span><br>            <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token comment">// [...]</span></code></pre>
<h2 id="summary">Summary</h2>
<p>And that’s it! To summarise, we:</p>
<ul>
<li>used the GitHub API to fetch a listing and contents of public gists for a specific user</li>
<li>created two proxy server scripts to authenticate with the GitHub API and fetch the data</li>
<li>added a CORS directive to the proxy server to allow our calling domain to request data</li>
<li>implemented basic caching to avoid unnecessary API data fetches</li>
<li>added a couple of npm modules to tranform gists written in Markdown to HTML, and highlight the syntax of gists comprising code. The syntax highlighting module was a custom-built npm dependency to avoid serving the user unnecessary JavaScript</li>
</ul>
<hr class="footnotes-sep">
<section class="footnotes">
<h2 class="h5">Footnotes</h2>
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>When <a href="https://github.com/settings/tokens">creating a personal access token</a>, if you only want to access public gists — as we do here — you don’t need to select any ‘scopes’: just name the token, choose an expiration date, and click ‘Generate token’. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>My home page isn’t popular enough to generate over 5,000 API requests an hour, but <a href="https://docs.github.com/en/rest/overview/resources-in-the-rest-api#conditional-requests">the GitHub documentation recommends checking headers for the last modification date</a> before making requests for new data. I thought it was worth following best practice as if I was building a high-traffic feature, as I may be doing in future work. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Keeping audio and visuals in sync with the Web Audio API</title>
      <link href="https://www.jamieonkeys.dev/posts/web-audio-api-output-latency/"/>
      <updated>2022-06-30T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/web-audio-api-output-latency/</id>
      <content type="html">
        <![CDATA[
      <p>There was an issue with the <a href="/posts/piano-keyboard-javascript/">animated piano keyboard</a> I built for my website <a href="https://www.jazzkeys.fyi/">JazzKeys.fyi</a> in that the audio and visuals were out of sync when listening on Bluetooth headphones. I had assumed that latency would be taken care of automatically by the OS — there are no such issues when viewing an embedded HTML <code>&lt;video&gt;</code>, for example — but it seems that the Web Audio API does not handle this by default.</p>
<p>It <a href="https://web.dev/audio-output-latency/">turns out</a>, however, that the Web Audio API’s <code>AudioContext</code> interface has a new property that returns an estimate in seconds of the output latency. The property, <a href="https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/outputLatency"><code>outputLatency</code></a>, is currently <a href="https://caniuse.com/mdn-api_audiocontext_outputlatency">supported</a> in newer versions of Firefox and Chrome<sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup>, and allows us to delay starting the visuals and have them be in sync with the audio.</p>
<h2 id="testing-it-out">Testing it out</h2>
<p>You can test it by running the code below in the browser console.</p>
<p>First check the latency while your audio output is set to the built-in speakers or a pair of wired headphones:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">var</span> audioCtx <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">AudioContext</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>audioCtx<span class="token punctuation">.</span>outputLatency</code></pre>
<p>I get 0 (zero) when I just checked in Firefox on macOS.<sup class="footnote-ref"><a href="#fn2" id="fnref2">2</a></sup></p>
<p>Now set the audio output to a pair of Bluetooth headphones and check the latency again:</p>
<pre class="language-javascript"><code class="language-javascript">audioCtx<span class="token punctuation">.</span>outputLatency</code></pre>
<p>I get 0.17780041666666666 seconds.</p>
<p>If I then switch back to the built-in speaker it‘s 0.02485258333333333 seconds. I can’t explain that; feel free to leave a comment if you know the reason.</p>
<figure>
    <img src="/img/outputlatency-tests-firefox.jpeg" width="601" height="198" alt="Testing the outputLatency property of the Web Audio API AudioContext when interface the audio output is set to the MacBook’s built-in speakers vs AirPods.">
    <figcaption>Testing the <code>outputLatency</code> property of the Web Audio API <code>AudioContext</code> interface when the audio output is set to the MacBook’s built-in speakers vs AirPods.</figcaption>
</figure>
<p>You can alternatively test it on an updated version of the animated keyboard (below). Output latency will be shown beneath the controls when you press Play.</p>
<figure class="w-950">
  <iframe src="https://codesandbox.io/embed/onscreen-piano-keyboard-latency-7mx7ut?autoresize=1&fontsize=14&hidenavigation=1&hidedevtools=1&theme=light&view=preview"
    class="shadow"
    style="width: 100%; height:500px; border:0; overflow:hidden;"
    title="onscreen-piano-keyboard"
    allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
    sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts hidedevtools"
   ></iframe>
  <figcaption>The audio and visuals should now be in sync no matter what the audio output is set to. In the code, we <a href="https://github.com/donbrae/onscreen-piano-keyboard/blob/main/src/index.js#L330">check the latency</a> each time the user clicks the Play button and <a href="https://github.com/donbrae/onscreen-piano-keyboard/blob/main/src/index.js#L378">pass this value to the Tone.js’s <code>Transport.Schedule()</code> function</a>, delaying the start of the animations so that they’re in sync with the audio.</figcaption>
</figure>
<h2 id="safari-and-older-browsers">Safari and older browsers</h2>
<p>Because browser support is incomplete, when using <code>outputLatency</code> we should first check that the property exists:<sup class="footnote-ref"><a href="#fn3" id="fnref3">3</a></sup></p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> hasOutputLatency <span class="token operator">=</span> window<span class="token punctuation">.</span>AudioContext <span class="token operator">&amp;&amp;</span> <span class="token string">'outputLatency'</span> <span class="token keyword">in</span> window<span class="token punctuation">.</span><span class="token class-name">AudioContext</span><span class="token punctuation">.</span>prototype <span class="token operator">?</span> <span class="token boolean">true</span> <span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">;</span><br><br><span class="token keyword">if</span> <span class="token punctuation">(</span>hasOutputLatency<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'AudioContext.outputLatency is available'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>Regarding Safari, I see there is a <a href="https://github.com/WebKit/WebKit/blob/cbf93f016e9697a706929e9b8bdc7a243041e0f8/Source/WebCore/Modules/webaudio/AudioContext.idl#L38-L39">fixme in the WebKit code</a>, so hopefully the feature will be available soon on iPhones and iPads.</p>
<h2 id="further-reading">Further reading</h2>
<p>There is some great technical background in <a href="https://blog.paul.cx/post/audio-video-synchronization-with-the-web-audio-api/">this post by a Paul Adenot</a>, an engineer at Mozilla who added <code>outputLatency</code> (along with <code>baseLatency</code> and <code>getOutputTimestamp</code>) to Firefox.</p>
<hr>
<p class="footnotes">Related post: <a href="/posts/logic-ascore-font/">Fix for missing ‘AScore’ font when editing Logic Pro PDFs</a>.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<h2 class="h5">Footnotes</h2>
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>This does not include Firefox and Chrome on iOS and iPadOS: they use WebKit under the hood. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>When <code>outputLatency</code> is queried in the <a href="https://codesandbox.io/s/onscreen-piano-keyboard-latency-7mx7ut">Codesandbox of my animated keyboard</a>, the (non-Bluetooth headphones) value returned is 0.0154195s in Firefox and 0.024s in Chrome, so it looks like it depends on browser and maybe what else is running in the program context (the Codesandbox has a whole bunch of other JavaScript running). It can also <a href="https://blog.paul.cx/post/audio-video-synchronization-with-the-web-audio-api/#now-the-catch">vary by OS and device</a> <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>If you want to ignore Internet Explorer, you can remove the check for <code>window.AudioContext</code> <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>A calendar and weather forecast in the terminal</title>
      <link href="https://www.jamieonkeys.dev/posts/calendar-and-weather-in-terminal/"/>
      <updated>2022-06-01T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/calendar-and-weather-in-terminal/</id>
      <content type="html">
        <![CDATA[
      <p>I learned a couple of cool terminal tricks on Hacker News yesterday.</p>
<h2 id="calendar">Calendar</h2>
<p>The <a href="https://news.ycombinator.com/item?id=31575076">first</a> is the <a href="https://en.wikipedia.org/wiki/Cal_(command)"><code>cal</code> program</a>, which prints an ASCII calendar.</p>
<p><code>cal</code> by itself returns the current month with today’s date highlighted.</p>
<p><code>cal 2022</code> returns a calendar for the whole of 2022.</p>
<p><code>cal -3</code> returns the current and surrounding months. This may be the most useful default view for me:</p>
<figure>
  <img src="/img/cal-3.jpg" width="714" height="221" alt="Running 'cal -3' returns a calendar comprising the previous month, current month and next month.">
  <figcaption>Running <code>cal -3</code> returns a calendar comprising the previous month, current month and next month.</figcaption>
</figure>
<p>It also works for the past: <code>cal 1981</code> returns a calendar for 1981. You can also capture just a month: <code>cal may 1981</code>.</p>
<h2 id="the-weather">The weather</h2>
<p><a href="https://news.ycombinator.com/item?id=31576344">Another user brought up <code>wttr.in</code></a>, a service which provides a <a href="https://github.com/chubin/wttr.in">weather report in ASCII format</a>.</p>
<p>You can run a simple <code>curl wttr.in</code> to retrieve a forecast based on your location:</p>
<figure>
  <img src="/img/terminal-weather.jpg" width="714" height="507" alt="A three-day weather forecast for Edinburgh.">
  <figcaption>A three-day weather forecast for Edinburgh.</figcaption>
</figure>
<h3 id="assigning-alias-weather-to-curl-wttr.in">Assigning alias <code>weather</code> to <code>curl wttr.in</code></h3>
<p>To make the <code>curl wttr.in</code> command more memorable, I decided to assign the alias <code>weather</code> to it. Adding aliases can be useful in general, so I thought I’d document the process:<sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup></p>
<ol>
<li><code>cd</code> to your home directory: <code>cd ~/</code></li>
<li>Open the file .bashrc in your preferred text editor (which is <a href="https://www.nano-editor.org">nano</a> for me): <code>sudo nano .bashrc</code></li>
<li>Add a new line with an alias: <code>alias weather='curl wttr.in'</code></li>
<li>Save the change (<kbd>Control</kbd>-<kbd>O</kbd> in nano), then exit (<kbd>Control</kbd>+<kbd>X</kbd>)</li>
<li>Run <code>source .bashrc</code> to add the function to the current terminal session<sup class="footnote-ref"><a href="#fn2" id="fnref2">2</a></sup></li>
<li>You can now run <code>weather</code> to get a weather forecast for your current location</li>
</ol>
<h3 id="passing-an-argument-for-location">Passing an argument for location</h3>
<p><a href="http://wttr.in">wttr.in</a> also lets you query the weather in a <a href="https://github.com/chubin/wttr.in#usage">specific location</a>. I updated my <code>weather</code> command to allow for this:</p>
<ol>
<li>In your home folder, run <code>unalias weather</code> to remove the alias we just set</li>
<li>Open .bashrc in your text editor</li>
<li>Delete the line with <code>alias weather='curl wttr.in'</code></li>
<li>Add a <code>weather()</code> function (you don’t need the <code>alias</code> keyword):</li>
</ol>
<pre class="language-bash"><code class="language-bash"><span class="token function-name function">weather</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$#</span> -eq <span class="token number">0</span> <span class="token punctuation">]</span> <span class="token comment"># If no argument has been passed to this function</span><br>    <span class="token keyword">then</span><br>      <span class="token function">curl</span> wttr.in<br>    <span class="token keyword">else</span><br>      <span class="token function">curl</span> wttr.in/<span class="token string">"<span class="token variable">$1</span>"</span> <span class="token comment"># Append location</span><br>  <span class="token keyword">fi</span><br><span class="token punctuation">}</span></code></pre>
<ol start="5">
<li>Save and exit</li>
<li>Run <code>source .bashrc</code></li>
<li>Now, by adding a location argument to <code>weather</code> you can see what the weather is like in, say, Glasgow (<code>weather glasgow+scotland</code>) or New York (<code>weather new+york</code>).<sup class="footnote-ref"><a href="#fn3" id="fnref3">3</a></sup> <code>weather</code> with no argument returns a forecast based on your location.</li>
</ol>
<hr class="footnotes-sep">
<section class="footnotes">
<h2 class="h5">Footnotes</h2>
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>I’m running macOS Big Sur, but this process, or something similar, should work on Linux too. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p><a href="https://askubuntu.com/questions/1266196/why-do-i-need-to-run-source-command-for-bashrc-alias-to-get-applied">More details on <code>source .bashrc</code> here</a>. If it’s not there already, you’ll need to add something along the lines of <code>if [ -f .bashrc ]; then source .bashrc; fi</code> to .zshrc or .bash_profile so that aliases are available to you in the terminal automatically whenever you log into the OS. In my case I also had to create the .zshrc file. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p><code>weather &quot;glasgow scotland&quot;</code> and <code>weather &quot;new york&quot;</code> will also work. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Building an animated piano keyboard with JavaScript and MIDI</title>
      <link href="https://www.jamieonkeys.dev/posts/piano-keyboard-javascript/"/>
      <updated>2022-05-12T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/piano-keyboard-javascript/</id>
      <content type="html">
        <![CDATA[
      <p>I’m currently working on <a href="https://www.jazzkeys.fyi">JazzKeys.fyi</a>, a website of jazz piano tutorials. I wanted each musical example to include a simple on-screen keyboard that would animate as notes are being played.</p>
<p>There didn’t seem to a solution that matched quite what I was looking for, so I decided to roll my own, a demo of which you interact with below.</p>
<figure class="w-950">
  <iframe src="https://codesandbox.io/embed/onscreen-piano-keyboard-3uhrm?autoresize=1&fontsize=14&hidenavigation=1&hidedevtools=1&theme=light&view=preview"
    class="shadow"
    style="width: 100%; height:500px; border:0; overflow:hidden;"
    title="onscreen-piano-keyboard"
    allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
    sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts hidedevtools"
   ></iframe>
  <figcaption>A working demo of the keyboard in action. Drag the vertical resizer bar to the right to reveal the (editable) code.</figcaption>
</figure>
<h2 id="approach">Approach</h2>
<p>I wanted the solution to be no more complex than it needed to be, so I started with a <a href="https://commons.wikimedia.org/wiki/File:PianoKeyboard.svg">piano keyboard SVG</a> I found on Wikimedia Commons. I extended the graphic to cover all 88 keys, rounded the key edges, and added some gradients.</p>
<p>My idea was to have the program play an audio file and update the <code>fill</code> property of whichever <code>&lt;rect&gt;</code>s in the SVG represented the keys that were being played.</p>
<h2 id="midi-to-json">MIDI to JSON</h2>
<p>MIDI was the obvious way to get the music (which I played and recorded in Logic) in the form of note-on, note-off and velocity data. I used <a href="https://tonejs.github.io">Tone.js</a> (‘a Web Audio framework for creating interactive music in the browser’) and <a href="https://github.com/Tonejs/Midi">Tone.js Midi</a> to covert MIDI file data into a JSON format that could be understood by Tone.js.</p>
<p>When the user clicks ‘Play’, the MIDI events are <a href="https://github.com/donbrae/onscreen-piano-keyboard/blob/main/src/index.js#L343-L380">scheduled by Tone.js</a>; then, when the audio file has loaded, the audio and MIDI files are <a href="https://github.com/donbrae/onscreen-piano-keyboard/blob/main/src/index.js#L260-L261">played simultaneously</a>.</p>
<p>There isn’t a synth hooked up to the Tone.js instance, so the MIDI file is, in effect, silent. Using <a href="https://tonejs.github.io/docs/r13/Draw">Tone.js’s <code>Draw</code> class</a>, you can schedule code to run on each MIDI event, and I use this to add and remove <code>note-on</code> class names to the relevant UI elements.</p>
<h2 id="audio">Audio</h2>
<p>I use <a href="https://howlerjs.com">Howler.js</a>, a library built on the Web Audio API, to handle the playing of audio. Here it plays the examples in either M4A or WebM formats, depending on the browser.</p>
<h2 id="ux">UX</h2>
<style type="text/css">.note-white,.note-black{stroke:#0a0a0a;}.note-white{fill:url('#GradientWhite');width:23px;height:120px;}.note-black{fill:url('#GradientBlack');width:13px;height:80px;}.middle-c{fill:#a6a6a6;}.top-line{stroke:#0a0a0a;stroke-width:2.1px;}.note.indicator{stroke:#494949;fill:#fff;stroke-width:1.7px;}.note.indicator.rh{stroke:#494949;fill:#494949;}</style>
<p><svg class="w-950 shadow brightness-dark-80" alt="A still from the piano keyboard animation" version="1.1" viewBox="0 0 1197.8 141.2" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="GradientBlack" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="#494949"></stop><stop offset="0.7" stop-color="#222" stop-opacity="1"></stop><stop offset="1" stop-color="#000"></stop></linearGradient><linearGradient id="GradientWhite" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="#dfdfdf"></stop><stop offset="0.5" stop-color="#f2f2f2" stop-opacity="1"></stop><stop offset="1" stop-color="#fff"></stop></linearGradient></defs><rect rx="1.5" y="20" x="0.5" class="note note-white keyA0"></rect><rect rx="1.5" y="20" x="23.5" class="note note-white keyB0"></rect><rect rx="1.5" y="20" x="20.2" class="note note-black keyA#0"></rect><rect rx="1.5" y="20" x="46.5" class="note note-white keyC1"></rect><rect rx="1.5" y="20" x="69.5" class="note note-white keyD1"></rect><rect rx="1.5" y="20" x="92.5" class="note note-white keyE1"></rect><rect rx="1.5" y="20" x="115.5" class="note note-white keyF1"></rect><rect rx="1.5" y="20" x="138.5" class="note note-white keyG1"></rect><rect rx="1.5" y="20" x="161.5" class="note note-white keyA1"></rect><rect rx="1.5" y="20" x="184.5" class="note note-white keyB1"></rect><rect rx="1.5" y="20" x="60.8" class="note note-black keyC#1"></rect><rect rx="1.5" y="20" x="88.2" class="note note-black keyD#1"></rect><rect rx="1.5" y="20" x="128.8" class="note note-black keyF#1"></rect><rect rx="1.5" y="20" x="154.8" class="note note-black keyG#1"></rect><rect rx="1.5" y="20" x="181.2" class="note note-black keyA#1"></rect><rect rx="1.5" y="20" x="207.5" class="note note-white keyC2"></rect><rect rx="1.5" y="20" x="230.5" class="note note-white keyD2"></rect><rect rx="1.5" y="20" x="253.5" class="note note-white keyE2"></rect><rect rx="1.5" y="20" x="276.5" class="note note-white keyF2"></rect><rect rx="1.5" y="20" x="299.5" class="note note-white keyG2"></rect><rect rx="1.5" y="20" x="322.5" class="note note-white keyA2"></rect><rect rx="1.5" y="20" x="345.5" class="note note-white keyB2"></rect><rect rx="1.5" y="20" x="221.8" class="note note-black keyC#2"></rect><rect rx="1.5" y="20" x="249.2" class="note note-black keyD#2"></rect><rect rx="1.5" y="20" x="289.8" class="note note-black keyF#2"></rect><rect rx="1.5" y="20" x="315.8" class="note note-black keyG#2"></rect><rect rx="1.5" y="20" x="342.2" class="note note-black keyA#2" style="fill:#5badfe"></rect><rect rx="1.5" y="20" x="368.5" class="note note-white keyC3"></rect><rect rx="1.5" y="20" x="391.5" class="note note-white keyD3"></rect><rect rx="1.5" y="20" x="414.5" class="note note-white keyE3"></rect><rect rx="1.5" y="20" x="437.5" class="note note-white keyF3"></rect><rect rx="1.5" y="20" x="460.5" class="note note-white keyG3"></rect><rect rx="1.5" y="20" x="483.5" class="note note-white keyA3"></rect><rect rx="1.5" y="20" x="506.5" class="note note-white keyB3"></rect><rect rx="1.5" y="20" x="382.8" class="note note-black keyC#3"></rect><rect rx="1.5" y="20" x="410.2" class="note note-black keyD#3"></rect><rect rx="1.5" y="20" x="450.8" class="note note-black keyF#3"></rect><rect rx="1.5" y="20" x="476.8" class="note note-black keyG#3"></rect><rect rx="1.5" y="20" x="503.2" class="note note-black keyA#3"></rect><rect rx="1.5" y="20" x="529.5" class="note note-white keyC4"></rect><rect rx="1.5" y="20" x="552.5" class="note note-white keyD4" style="fill:#feb1af"></rect><rect rx="1.5" y="20" x="575.5" class="note note-white keyE4"></rect><rect rx="1.5" y="20" x="598.5" class="note note-white keyF4"></rect><rect rx="1.5" y="20" x="621.5" class="note note-white keyG4"></rect><rect rx="1.5" y="20" x="644.5" class="note note-white keyA4"></rect><rect rx="1.5" y="20" x="667.5" class="note note-white keyB4"></rect><rect rx="1.5" y="20" x="543.8" class="note note-black keyC#4"></rect><rect rx="1.5" y="20" x="571.2" class="note note-black keyD#4"></rect><rect rx="1.5" y="20" x="611.8" class="note note-black keyF#4"></rect><rect rx="1.5" y="20" x="637.8" class="note note-black keyG#4" style="fill:#ff3e3d"></rect><rect rx="1.5" y="20" x="664.2" class="note note-black keyA#4"></rect><rect rx="1.5" y="20" x="690.52" class="note note-white keyC5"></rect><rect rx="1.5" y="20" x="713.52" class="note note-white keyD5"></rect><rect rx="1.5" y="20" x="736.52" class="note note-white keyE5"></rect><rect rx="1.5" y="20" x="759.52" class="note note-white keyF5"></rect><rect rx="1.5" y="20" x="782.52" class="note note-white keyG5"></rect><rect rx="1.5" y="20" x="805.52" class="note note-white keyA5"></rect><rect rx="1.5" y="20" x="828.52" class="note note-white keyB5"></rect><rect rx="1.5" y="20" x="704.82" class="note note-black keyC#5"></rect><rect rx="1.5" y="20" x="732.12" class="note note-black keyD#5"></rect><rect rx="1.5" y="20" x="772.72" class="note note-black keyF#5"></rect><rect rx="1.5" y="20" x="798.72" class="note note-black keyG#5"></rect><rect rx="1.5" y="20" x="825.22" class="note note-black keyA#5"></rect><rect rx="1.5" y="20" x="851.52" class="note note-white keyC6"></rect><rect rx="1.5" y="20" x="874.52" class="note note-white keyD6"></rect><rect rx="1.5" y="20" x="897.52" class="note note-white keyE6"></rect><rect rx="1.5" y="20" x="920.52" class="note note-white keyF6"></rect><rect rx="1.5" y="20" x="943.52" class="note note-white keyG6"></rect><rect rx="1.5" y="20" x="966.52" class="note note-white keyA6"></rect><rect rx="1.5" y="20" x="989.52" class="note note-white keyB6"></rect><rect rx="1.5" y="20" x="865.82" class="note note-black keyC#6"></rect><rect rx="1.5" y="20" x="893.12" class="note note-black keyD#6"></rect><rect rx="1.5" y="20" x="933.72" class="note note-black keyF#6"></rect><rect rx="1.5" y="20" x="959.72" class="note note-black keyG#6"></rect><rect rx="1.5" y="20" x="986.22" class="note note-black keyA#6"></rect><rect rx="1.5" y="20" x="1012.52" class="note note-white keyC7"></rect><rect rx="1.5" y="20" x="1035.52" class="note note-white keyD7"></rect><rect rx="1.5" y="20" x="1058.52" class="note note-white keyE7"></rect><rect rx="1.5" y="20" x="1081.52" class="note note-white keyF7"></rect><rect rx="1.5" y="20" x="1104.52" class="note note-white keyG7"></rect><rect rx="1.5" y="20" x="1127.52" class="note note-white keyA7"></rect><rect rx="1.5" y="20" x="1150.52" class="note note-white keyB7"></rect><rect rx="1.5" y="20" x="1026.82" class="note note-black keyC#7"></rect><rect rx="1.5" y="20" x="1054.12" class="note note-black keyD#7"></rect><rect rx="1.5" y="20" x="1094.72" class="note note-black keyF#7"></rect><rect rx="1.5" y="20" x="1120.72" class="note note-black keyG#7"></rect><rect rx="1.5" y="20" x="1147.22" class="note note-black keyA#7"></rect><rect rx="1.5" y="20" x="1173.52" class="note note-white keyC8"></rect><circle r="5.7" class="middle-c" cx="541.1" cy="125"></circle><circle cx="348.7" cy="9" r="5.7" class="note indicator keyA#2" style="stroke:#999"></circle><circle cx="550.2" cy="9" r="5.7" class="note indicator keyC#4" style="stroke:#d5d5d5;fill:#d5d5d5"></circle><circle cx="563.9" cy="9" r="5.7" class="note indicator keyD4" style="fill:#616161;stroke:#616161"></circle><circle cy="9" r="5.7" class="note indicator keyA4" fill="#4e4e4e" style="fill:#4e4e4e;stroke:#4e4e4e" cx="644.3"></circle><line class="top-line" x2="1197" x1="0" y2="20" y1="20"></line></svg></p>
<p>The colour red is used to show what the right hand is playing, and blue the left. The brighter the colour, the higher the velocity (i.e. the speed with which the key is being depressed).<sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup></p>
<p>Additionally, a circle appears above each key as it’s played: a filled-in circle for the right hand, and a circle outline for the left. The circles have a ~1s fade-out to help orient the user. (By orient I mean that when a given note starts playing, there is an indication to the user as to which note or notes were played immediately before.)</p>
<figure class="w-950">
  <svg class="groove_funk_1 music-notation" viewBox="0 0 2148.11 1116.106" xmlns="http://www.w3.org/2000/svg"><g class="staff-1"><polygon points="33.5 719.1 33.5 719.6 2145.5 719.6 2145.5 718.6 33.5 718.6 33.5 719.6 33.5 719.1 33.5 719.6 2145.5 719.6 2145.5 718.6 33.5 718.6 33.5 719.6 33.5 719.1"></polygon><polygon points="33.5 741.1 33.5 741.6 2145.5 741.6 2145.5 740.6 33.5 740.6 33.5 741.6 33.5 741.1 33.5 741.6 2145.5 741.6 2145.5 740.6 33.5 740.6 33.5 741.6 33.5 741.1"></polygon><polygon points="33.5 763.1 33.5 763.6 2145.5 763.6 2145.5 762.6 33.5 762.6 33.5 763.6 33.5 763.1 33.5 763.6 2145.5 763.6 2145.5 762.6 33.5 762.6 33.5 763.6 33.5 763.1"></polygon><polygon points="33.5 785.1 33.5 785.6 2145.5 785.6 2145.5 784.6 33.5 784.6 33.5 785.6 33.5 785.1 33.5 785.6 2145.5 785.6 2145.5 784.6 33.5 784.6 33.5 785.6 33.5 785.1"></polygon><polygon points="33.5 807.1 33.5 807.6 2145.5 807.6 2145.5 806.6 33.5 806.6 33.5 807.6 33.5 807.1 33.5 807.6 2145.5 807.6 2145.5 806.6 33.5 806.6 33.5 807.6 33.5 807.1"></polygon><polygon points="33.5 719.1 29.5 719.1 29.5 807.1 37.5 807.1 37.5 719.1 29.5 719.1 33.5 719.1 29.5 719.1 29.5 807.1 37.5 807.1 37.5 719.1 29.5 719.1 33.5 719.1"></polygon><path d="M116.48,866.27a11.67,11.67,0,0,1-3.43-.44q0-5.19,4.48-12.85,4.32-5.81,6.69-8.44,1.41,13.46,2.29,20.15A61.38,61.38,0,0,1,116.48,866.27Zm.35-28.6q-9.15,14.18-9.15,27.46l-2.91-1.06q-6.42-2.37-6.42-11,0-10.82,20.33-55.88l3,33.53A84.38,84.38,0,0,1,116.83,837.67Zm25,14.7q-6.69,8.45-12,10.29-.18-3-1-10.2-.7-6.69-.71-10.3,0-2.2,5.64-5.19,5.19-2.73,7.74-2.73a10.41,10.41,0,0,1,4.4,1.06c1.76.88,2.64,1.93,2.64,3.16Q148.51,843.92,141.82,852.37ZM122.2,766.3l-2.47,8.19a122.65,122.65,0,0,1-2.11-21.3c0-1.35.35-2,1.06-2q1.48,0,3.3,2.73a8.78,8.78,0,0,1,1.8,4.31A31.73,31.73,0,0,1,122.2,766.3Zm30.18,59.76a11.14,11.14,0,0,0-10.56-6.34,19.33,19.33,0,0,0-8.8,2.46,27.08,27.08,0,0,0-7.3,5.55q-.61-1.68-2.11-19.1-1.41-16.8-1.41-20.85a38.29,38.29,0,0,1,1.49-3.7q6.07-14.16,6.08-24.11,0-19.54-11.62-27.81a20.57,20.57,0,0,0-4-2c-1.53,0-2.61,1.11-3.26,3.34l5.55,44.35q.09.79-5.81,15-9.6,23.14-12.5,31.68-5.9,17.52-5.89,27.81,0,14.07,6.51,22.35,7.21,9,20.94,9a48,48,0,0,0,6.52-.44,20,20,0,0,1,2.9-.62c.47,0,.85,1,1.14,2.9a23.29,23.29,0,0,1,.36,3.61q0,9.42-6.43,20.33a35.56,35.56,0,0,0,.53,6.16q.8,4.75,2.38,4.75,4.31,0,6.86-11.26a69.67,69.67,0,0,0,1.76-15,53.53,53.53,0,0,0-1.06-9.42c-.47-2.46-.73-3.84-.79-4.13a3.56,3.56,0,0,0,.44-.35,47.9,47.9,0,0,0,6-4.32A40.37,40.37,0,0,0,151,861a56.4,56.4,0,0,0,2.64-10.91,69.62,69.62,0,0,0,1.32-11.26Q154.93,830.9,152.38,826.06Z" transform="translate(-45 -62.4)"></path><path d="M191.11,818.81q0,8.1-9.24,16.63l.27-10c.05-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S191.11,817.11,191.11,818.81ZM182.84,812l.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7-11.61,7-20.68,0-8.72-5.1-8.71Q186.8,805.26,182.84,812Z" transform="translate(-45 -62.4)"></path><path d="M213.11,785.81q0,8.1-9.24,16.63l.27-10c.05-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S213.11,784.11,213.11,785.81ZM204.84,779l.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7-11.61,7-20.68,0-8.72-5.1-8.71Q208.8,772.26,204.84,779Z" transform="translate(-45 -62.4)"></path><polygon points="2145.5 719.1 2141.5 719.1 2141.5 807.1 2149.5 807.1 2149.5 719.1 2141.5 719.1 2145.5 719.1 2141.5 719.1 2141.5 807.1 2149.5 807.1 2149.5 719.1 2141.5 719.1 2145.5 719.1"></polygon><path d="M69,781q-24,9.69-24,35.53t8,44.18q8,24.45,8,44.18T45,942.5q16,17.89,16,37.61t-8,44.18q-8,18.35-8,44.18T69,1104q-16-9.69-16-35.53,4-25.85,12-44.18,8-24.45,8-44.18-4-19.73-26-37.61,26-17.89,26-37.61t-8-44.18a400.62,400.62,0,0,1-12-44.18Q53,790.69,69,781Z" transform="translate(-45 -62.4)"></path><polygon points="33.5 719.1 29.5 719.1 29.5 1042.1 37.5 1042.1 37.5 719.1 29.5 719.1 33.5 719.1 29.5 719.1 29.5 1042.1 37.5 1042.1 37.5 719.1 29.5 719.1 33.5 719.1"></polygon><polygon points="33.5 954.1 33.5 954.6 2145.5 954.6 2145.5 953.6 33.5 953.6 33.5 954.6 33.5 954.1 33.5 954.6 2145.5 954.6 2145.5 953.6 33.5 953.6 33.5 954.6 33.5 954.1"></polygon><polygon points="33.5 976.1 33.5 976.6 2145.5 976.6 2145.5 975.6 33.5 975.6 33.5 976.6 33.5 976.1 33.5 976.6 2145.5 976.6 2145.5 975.6 33.5 975.6 33.5 976.6 33.5 976.1"></polygon><polygon points="33.5 998.1 33.5 998.6 2145.5 998.6 2145.5 997.6 33.5 997.6 33.5 998.6 33.5 998.1 33.5 998.6 2145.5 998.6 2145.5 997.6 33.5 997.6 33.5 998.6 33.5 998.1"></polygon><polygon points="33.5 1020.1 33.5 1020.6 2145.5 1020.6 2145.5 1019.6 33.5 1019.6 33.5 1020.6 33.5 1020.1 33.5 1020.6 2145.5 1020.6 2145.5 1019.6 33.5 1019.6 33.5 1020.6 33.5 1020.1"></polygon><polygon points="33.5 1042.1 33.5 1042.6 2145.5 1042.6 2145.5 1041.6 33.5 1041.6 33.5 1042.6 33.5 1042.1 33.5 1042.6 2145.5 1042.6 2145.5 1041.6 33.5 1041.6 33.5 1042.6 33.5 1042.1"></polygon><polygon points="33.5 719.1 29.5 719.1 29.5 1042.1 37.5 1042.1 37.5 719.1 29.5 719.1 33.5 719.1 29.5 719.1 29.5 1042.1 37.5 1042.1 37.5 719.1 29.5 719.1 33.5 719.1"></polygon><path d="M171.21,1042.14a42.63,42.63,0,0,0-4.84.44c-2.58.29-4,.58-4.31.88-.64.64-1.35,2.24-2.11,4.79a22.35,22.35,0,0,0-1.14,5.5c0,1.94.93,2.91,2.81,2.91.59,0,2-.17,4.4-.49a39.12,39.12,0,0,0,4-.66q1.59-.62,2.55-4.57a23.49,23.49,0,0,0,.79-5.64C173.41,1043.19,172.68,1042.14,171.21,1042.14Zm0-23.24a45.18,45.18,0,0,0-4.84.44c-2.58.3-4,.59-4.31.88-.64.65-1.35,2.25-2.11,4.8a22.35,22.35,0,0,0-1.14,5.5c0,1.94.93,2.9,2.81,2.9.59,0,2-.16,4.4-.48a39.12,39.12,0,0,0,4-.66q1.59-.62,2.55-4.58a23.35,23.35,0,0,0,.79-5.63C173.41,1020,172.68,1018.9,171.21,1018.9Zm-48.48,87.21q7.47-6,10.56-10,5.44-7,6.07-8a59.93,59.93,0,0,0,7.57-18.3,84.4,84.4,0,0,0,2.37-19.8q0-15-6.25-25.61-7.56-12.76-21.56-12.76-13.36,0-21.2,12.41-6.69,10.65-6.69,24.81,0,9.08,5.72,13.64a6.73,6.73,0,0,0,1.76.88c1.58,0,2.37-2.72,2.37-8.18a8.78,8.78,0,0,0-2.5-6.42c-1.68-1.71-2.51-3-2.51-3.79q0-5.54,7.48-11,6.94-5.1,12.76-5.1,18.21,0,24,16.54a24.13,24.13,0,0,1,1.59,7.92q0,13.38-10.92,27a63.79,63.79,0,0,1-23.32,18.57,53.73,53.73,0,0,0-1.4,12.84v.36a5,5,0,0,0,2.11.79Q114.36,1112.89,122.73,1106.11Z" transform="translate(-45 -62.4)"></path><path d="M191.11,1075.81q0,8.1-9.24,16.63l.27-10c.05-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S191.11,1074.11,191.11,1075.81Zm-8.27-6.77.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7-11.61,7-20.68,0-8.71-5.1-8.71Q186.8,1062.26,182.84,1069Z" transform="translate(-45 -62.4)"></path><path d="M213.11,1042.81q0,8.1-9.24,16.63l.27-10c.05-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S213.11,1041.11,213.11,1042.81Zm-8.27-6.77.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7-11.61,7-20.68,0-8.71-5.1-8.71Q208.8,1029.26,204.84,1036Z" transform="translate(-45 -62.4)"></path><polygon points="2145.5 719.1 2141.5 719.1 2141.5 1042.1 2149.5 1042.1 2149.5 719.1 2141.5 719.1 2145.5 719.1 2141.5 719.1 2141.5 1042.1 2149.5 1042.1 2149.5 719.1 2141.5 719.1 2145.5 719.1"></polygon></g><g class="staff-0"><polygon points="33.5 155.1 33.5 155.6 2145.5 155.6 2145.5 154.6 33.5 154.6 33.5 155.6 33.5 155.1 33.5 155.6 2145.5 155.6 2145.5 154.6 33.5 154.6 33.5 155.6 33.5 155.1"></polygon><polygon points="33.5 478.1 33.5 478.6 2145.5 478.6 2145.5 477.6 33.5 477.6 33.5 478.6 33.5 478.1 33.5 478.6 2145.5 478.6 2145.5 477.6 33.5 477.6 33.5 478.6 33.5 478.1"></polygon><polygon points="33.5 177.1 33.5 177.6 2145.5 177.6 2145.5 176.6 33.5 176.6 33.5 177.6 33.5 177.1 33.5 177.6 2145.5 177.6 2145.5 176.6 33.5 176.6 33.5 177.6 33.5 177.1"></polygon><polygon points="33.5 199.1 33.5 199.6 2145.5 199.6 2145.5 198.6 33.5 198.6 33.5 199.6 33.5 199.1 33.5 199.6 2145.5 199.6 2145.5 198.6 33.5 198.6 33.5 199.6 33.5 199.1"></polygon><polygon points="33.5 221.1 33.5 221.6 2145.5 221.6 2145.5 220.6 33.5 220.6 33.5 221.6 33.5 221.1 33.5 221.6 2145.5 221.6 2145.5 220.6 33.5 220.6 33.5 221.6 33.5 221.1"></polygon><polygon points="33.5 243.1 33.5 243.6 2145.5 243.6 2145.5 242.6 33.5 242.6 33.5 243.6 33.5 243.1 33.5 243.6 2145.5 243.6 2145.5 242.6 33.5 242.6 33.5 243.6 33.5 243.1"></polygon><polygon points="33.5 155.1 29.5 155.1 29.5 243.1 37.5 243.1 37.5 155.1 29.5 155.1 33.5 155.1 29.5 155.1 29.5 243.1 37.5 243.1 37.5 155.1 29.5 155.1 33.5 155.1"></polygon><path d="M116.48,302.27a11.67,11.67,0,0,1-3.43-.44q0-5.19,4.48-12.85,4.32-5.81,6.69-8.44,1.41,13.45,2.29,20.15A61.38,61.38,0,0,1,116.48,302.27Zm.35-28.6q-9.15,14.18-9.15,27.46l-2.91-1.06q-6.42-2.37-6.42-11,0-10.81,20.33-55.88l3,33.53A84.38,84.38,0,0,1,116.83,273.67Zm25,14.7q-6.69,8.44-12,10.29-.18-3-1-10.2-.7-6.69-.71-10.3,0-2.21,5.64-5.19,5.19-2.73,7.74-2.73a10.41,10.41,0,0,1,4.4,1.06c1.76.88,2.64,1.93,2.64,3.16Q148.51,279.92,141.82,288.37ZM122.2,202.3l-2.47,8.19a122.65,122.65,0,0,1-2.11-21.3c0-1.35.35-2,1.06-2q1.48,0,3.3,2.73a8.78,8.78,0,0,1,1.8,4.31A31.73,31.73,0,0,1,122.2,202.3Zm30.18,59.76a11.14,11.14,0,0,0-10.56-6.34,19.33,19.33,0,0,0-8.8,2.46,27.08,27.08,0,0,0-7.3,5.55q-.61-1.68-2.11-19.1-1.41-16.8-1.41-20.85a38.29,38.29,0,0,1,1.49-3.7q6.07-14.16,6.08-24.11,0-19.54-11.62-27.81a20.57,20.57,0,0,0-4-2c-1.53,0-2.61,1.11-3.26,3.34l5.55,44.35q.09.79-5.81,15-9.6,23.15-12.5,31.68-5.9,17.52-5.89,27.81,0,14.07,6.51,22.35,7.21,9,20.94,9a48,48,0,0,0,6.52-.44,20,20,0,0,1,2.9-.62c.47,0,.85,1,1.14,2.9a23.29,23.29,0,0,1,.36,3.61q0,9.42-6.43,20.33a35.56,35.56,0,0,0,.53,6.16q.8,4.75,2.38,4.75,4.31,0,6.86-11.26a69.67,69.67,0,0,0,1.76-15.05,53.53,53.53,0,0,0-1.06-9.42c-.47-2.46-.73-3.84-.79-4.13a3.56,3.56,0,0,0,.44-.35,47.9,47.9,0,0,0,6-4.32A40.37,40.37,0,0,0,151,297a56.4,56.4,0,0,0,2.64-10.91,69.62,69.62,0,0,0,1.32-11.26Q154.93,266.9,152.38,262.06Z" transform="translate(-45 -62.4)"></path><path d="M191.11,254.81q0,8.1-9.24,16.63l.27-10c.05-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S191.11,253.11,191.11,254.81ZM182.84,248l.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7-11.61,7-20.68,0-8.72-5.1-8.71Q186.8,241.26,182.84,248Z" transform="translate(-45 -62.4)"></path><path d="M213.11,221.81q0,8.1-9.24,16.63l.27-10c.05-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S213.11,220.11,213.11,221.81ZM204.84,215l.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7-11.61,7-20.68,0-8.72-5.1-8.71Q208.8,208.26,204.84,215Z" transform="translate(-45 -62.4)"></path><path d="M269.82,260.65h-2q-6.69,7-7.57,7.83a14.27,14.27,0,0,1-10.12,4c-3.63,0-6.13-1-7.48-2.9q-1.76-2.39-1.76-8a23.66,23.66,0,0,1,2.64-10.78,18.34,18.34,0,0,1,7.22-7.88q2.64,0,3.78,5.19c.24,1.24.53,3.67.88,7.31h1.94a13.2,13.2,0,0,0,2.82-5,26.79,26.79,0,0,0,.44-5.89,27.29,27.29,0,0,0-1.32-8.36c-1.24-3.82-3-5.72-5.2-5.72q-9.06,0-14.34,15.31a70.65,70.65,0,0,0-4.05,23.23q0,8,2.29,11.79,3,4.85,10.47,4.84,6.6,0,14.43-6.16,8.28-6.51,8.28-12.85A18,18,0,0,0,269.82,260.65Z" transform="translate(-45 -62.4)"></path><polygon points="2145.5 155.1 2141.5 155.1 2141.5 243.1 2149.5 243.1 2149.5 155.1 2141.5 155.1 2145.5 155.1 2141.5 155.1 2141.5 243.1 2149.5 243.1 2149.5 155.1 2141.5 155.1 2145.5 155.1"></polygon><path d="M69,217q-24,9.69-24,35.53t8,44.18q8,24.45,8,44.18T45,378.5q16,17.89,16,37.61t-8,44.18q-8,18.34-8,44.18T69,540q-16-9.69-16-35.53,4-25.85,12-44.18,8-24.45,8-44.18-4-19.73-26-37.61,26-17.89,26-37.61t-8-44.18a400.62,400.62,0,0,1-12-44.18Q53,226.69,69,217Z" transform="translate(-45 -62.4)"></path><polygon points="33.5 155.1 29.5 155.1 29.5 478.1 37.5 478.1 37.5 155.1 29.5 155.1 33.5 155.1 29.5 155.1 29.5 478.1 37.5 478.1 37.5 155.1 29.5 155.1 33.5 155.1"></polygon><polygon points="33.5 390.1 33.5 390.6 2145.5 390.6 2145.5 389.6 33.5 389.6 33.5 390.6 33.5 390.1 33.5 390.6 2145.5 390.6 2145.5 389.6 33.5 389.6 33.5 390.6 33.5 390.1"></polygon><polygon points="33.5 412.1 33.5 412.6 2145.5 412.6 2145.5 411.6 33.5 411.6 33.5 412.6 33.5 412.1 33.5 412.6 2145.5 412.6 2145.5 411.6 33.5 411.6 33.5 412.6 33.5 412.1"></polygon><polygon points="33.5 434.1 33.5 434.6 2145.5 434.6 2145.5 433.6 33.5 433.6 33.5 434.6 33.5 434.1 33.5 434.6 2145.5 434.6 2145.5 433.6 33.5 433.6 33.5 434.6 33.5 434.1"></polygon><polygon points="33.5 456.1 33.5 456.6 2145.5 456.6 2145.5 455.6 33.5 455.6 33.5 456.6 33.5 456.1 33.5 456.6 2145.5 456.6 2145.5 455.6 33.5 455.6 33.5 456.6 33.5 456.1"></polygon><polygon points="33.5 155.1 29.5 155.1 29.5 478.1 37.5 478.1 37.5 155.1 29.5 155.1 33.5 155.1 29.5 155.1 29.5 478.1 37.5 478.1 37.5 155.1 29.5 155.1 33.5 155.1"></polygon><path d="M171.21,478.14a42.63,42.63,0,0,0-4.84.44c-2.58.29-4,.58-4.31.88-.64.64-1.35,2.24-2.11,4.79a22.35,22.35,0,0,0-1.14,5.5c0,1.94.93,2.91,2.81,2.91.59,0,2-.17,4.4-.49a39.12,39.12,0,0,0,4-.66q1.59-.61,2.55-4.57a23.49,23.49,0,0,0,.79-5.64C173.41,479.19,172.68,478.14,171.21,478.14Zm0-23.24a45.18,45.18,0,0,0-4.84.44c-2.58.3-4,.59-4.31.88-.64.65-1.35,2.25-2.11,4.8a22.35,22.35,0,0,0-1.14,5.5c0,1.94.93,2.9,2.81,2.9.59,0,2-.16,4.4-.48a39.12,39.12,0,0,0,4-.66q1.59-.61,2.55-4.58a23.35,23.35,0,0,0,.79-5.63C173.41,456,172.68,454.9,171.21,454.9Zm-48.48,87.21q7.47-6,10.56-10,5.44-7,6.07-8a59.93,59.93,0,0,0,7.57-18.3A84.4,84.4,0,0,0,149.3,486q0-15-6.25-25.61-7.56-12.76-21.56-12.76-13.36,0-21.2,12.41-6.69,10.65-6.69,24.81,0,9.07,5.72,13.64a6.73,6.73,0,0,0,1.76.88c1.58,0,2.37-2.72,2.37-8.18a8.78,8.78,0,0,0-2.5-6.42c-1.68-1.71-2.51-3-2.51-3.79q0-5.54,7.48-11,6.94-5.1,12.76-5.1,18.21,0,24,16.54a24.13,24.13,0,0,1,1.59,7.92q0,13.38-10.92,27a63.79,63.79,0,0,1-23.32,18.57,53.73,53.73,0,0,0-1.4,12.84v.36a5,5,0,0,0,2.11.79Q114.36,548.89,122.73,542.11Z" transform="translate(-45 -62.4)"></path><path d="M191.11,511.81q0,8.1-9.24,16.63l.27-10c.05-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S191.11,510.11,191.11,511.81ZM182.84,505l.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7-11.61,7-20.68,0-8.72-5.1-8.71Q186.8,498.26,182.84,505Z" transform="translate(-45 -62.4)"></path><path d="M213.11,478.81q0,8.1-9.24,16.63l.27-10c.05-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S213.11,477.11,213.11,478.81ZM204.84,472l.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7-11.61,7-20.68,0-8.72-5.1-8.71Q208.8,465.26,204.84,472Z" transform="translate(-45 -62.4)"></path><path d="M269.82,495.65h-2q-6.69,7-7.57,7.83a14.27,14.27,0,0,1-10.12,4c-3.63,0-6.13-1-7.48-2.9q-1.76-2.39-1.76-8a23.66,23.66,0,0,1,2.64-10.78,18.34,18.34,0,0,1,7.22-7.88q2.64,0,3.78,5.19c.24,1.24.53,3.67.88,7.31h1.94a13.2,13.2,0,0,0,2.82-5,26.79,26.79,0,0,0,.44-5.89,27.29,27.29,0,0,0-1.32-8.36c-1.24-3.82-3-5.72-5.2-5.72q-9.06,0-14.34,15.31a70.65,70.65,0,0,0-4.05,23.23q0,8,2.29,11.79,3,4.85,10.47,4.84,6.6,0,14.43-6.16,8.28-6.51,8.28-12.85A18,18,0,0,0,269.82,495.65Z" transform="translate(-45 -62.4)"></path><polygon points="2145.5 155.1 2141.5 155.1 2141.5 478.1 2149.5 478.1 2149.5 155.1 2141.5 155.1 2145.5 155.1 2141.5 155.1 2141.5 478.1 2149.5 478.1 2149.5 155.1 2141.5 155.1 2145.5 155.1"></polygon></g><g class="bar bar-1"><path d="M263.66,845.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.56,8.56,0,0,1-1.4-5.28q0-3.26,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1.05,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,263.66,845.2Zm.8-13.73q.62-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,264.46,831.47Z" transform="translate(-45 -62.4)"></path><polygon points="309.25 829.1 309.25 830.1 339.75 830.1 339.75 828.1 309.25 828.1 309.25 830.1 309.25 829.1 309.25 830.1 339.75 830.1 339.75 828.1 309.25 828.1 309.25 830.1 309.25 829.1"></polygon><path d="M380.16,910.45a2.7,2.7,0,0,0-.35-1.59q-1.68,0-5-2.28t-3.34-3.88q0-.16,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.6,5.8-5.37,8.8-2.73-4.12-2.72-12.67a5.27,5.27,0,0,0-.18-2q-1.41,0-2.9,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.66,8.7-4.67,11c0,1.34.38,2,1.15,2q.87,0,2.81-3.7c1.41-2.7,2.35-4.16,2.82-4.4q2.81,2.64,4.13,4c1.7,1.59,3.32,2.38,4.84,2.38Q380.16,916.52,380.16,910.45Z" transform="translate(-45 -62.4)"></path><polygon points="332 715.1 329 715.1 329 829.1 335 829.1 335 715.1 329 715.1 332 715.1 329 715.1 329 829.1 335 829.1 335 715.1 329 715.1 332 715.1"></polygon><path d="M516.86,876.13q7.3-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q510.09,881.06,516.86,876.13Z" transform="translate(-45 -62.4)"></path><path d="M516.86,843.13q7.3-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q510.09,848.06,516.86,843.13Z" transform="translate(-45 -62.4)"></path><path d="M478.11,829.31q0,8.1-9.24,16.63l.27-10c.05-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S478.11,827.61,478.11,829.31Zm-8.27-6.77.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7.05-11.61,7-20.68,0-8.72-5.1-8.71Q473.8,815.76,469.84,822.54Z" transform="translate(-45 -62.4)"></path><polyline points="334.5 731.6 478.5 696.4 478.5 708.4 334.5 743.6"></polyline><polyline points="334.5 714.1 478.5 678.9 478.5 690.9 334.5 726.1"></polyline><polygon points="476 679.9 473 679.9 473 804.35 479 804.35 479 679.9 473 679.9 476 679.9 473 679.9 473 804.35 479 804.35 479 679.9 473 679.9 476 679.9"></polygon><path d="M612.66,845.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.56,8.56,0,0,1-1.4-5.28q0-3.26,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1.05,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,612.66,845.2Zm.8-13.73q.62-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,613.46,831.47Z" transform="translate(-45 -62.4)"></path><path d="M699,834.35q-5.71-11.07-13.53-13.87v12.58l4.42,3.4a34.41,34.41,0,0,1,7.75,7.62,17.77,17.77,0,0,1,3.2,10.2q0,5.91-2.18,8.63a3,3,0,0,0,2,.82q1.85,0,2.79-4.35a27.8,27.8,0,0,0,.68-6Q704.13,844.29,699,834.35Z" transform="translate(-45 -62.4)"></path><polygon points="638.5 760.35 635.5 760.35 635.5 815.35 641.5 815.35 641.5 760.35 635.5 760.35 638.5 760.35 635.5 760.35 635.5 815.35 641.5 815.35 641.5 760.35 635.5 760.35 638.5 760.35"></polygon><polygon points="619 800.23 620.34 802.91 664.34 780.91 661.66 775.54 617.66 797.54 620.34 802.91 619 800.23 620.34 802.91 664.34 780.91 661.66 775.54 617.66 797.54 620.34 802.91 619 800.23"></polygon><path d="M693.38,888.25q2.7,11,8.12,11t8.12-11q-2.7,5.91-8.12,5.91T693.38,888.25Z" transform="translate(-45 -62.4)"></path><path d="M680.79,885.51q5.64-4.15,5.64-8.5,0-6.33-8.23-6.33-4.35,0-9.52,4.56t-5.16,8.84a4.43,4.43,0,0,0,2.51,4.15,10.27,10.27,0,0,0,5,1.09Q675.55,889.32,680.79,885.51Z" transform="translate(-45 -62.4)"></path><path d="M652.13,874.83q0,6.25-7.14,12.85l.21-7.75q.06-1.29,2-4.18t3.44-2.89C651.63,872.86,652.13,873.52,652.13,874.83Zm-6.39-5.23.34-27.61a1.3,1.3,0,0,0-.54-.07,4,4,0,0,0-3.06,1.7l-.62,56.78a71.09,71.09,0,0,0,10.61-13.33q5.44-9,5.44-16,0-6.74-3.94-6.73Q648.79,864.36,645.74,869.6Z" transform="translate(-45 -62.4)"></path><path d="M773.86,887.13q7.3-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q767.09,892.06,773.86,887.13Z" transform="translate(-45 -62.4)"></path><path d="M729.06,881.32q-2.38,2.28-7.75,3.17l.35-5.37a16,16,0,0,1,7.66-3.78Zm-9.16,34.5a14.62,14.62,0,0,0-1.14-6.52q-5.19-9.76-6.51-13.11a37.63,37.63,0,0,1-3-14.17q0-4.31,2.46-12.32t2.46-12a22.72,22.72,0,0,0-1.4-6.52A70.49,70.49,0,0,0,707,879.47q0,21.39,12.14,44.09A46.38,46.38,0,0,0,719.9,915.82Zm2.38-47.26,1.32-28a7.06,7.06,0,0,1-1-.09,7.06,7.06,0,0,0-1-.09,5.75,5.75,0,0,1-1,.79l-4,53.59a6.27,6.27,0,0,0,2.2.36,12.8,12.8,0,0,0,5.32-1.5,9.47,9.47,0,0,0,4.18-3.26l-1.85,30.28a13,13,0,0,1,1.85.26c-.12,0,.44-.41,1.67-1.23q2.38-33.62,3.44-55.62Q724.39,866.62,722.28,868.56Zm20.5,15a93.37,93.37,0,0,0-3-23.06A81.47,81.47,0,0,0,731.17,839a30.15,30.15,0,0,0-1,6.87,33.72,33.72,0,0,0,.35,3.78,14.61,14.61,0,0,0,.79,3.61q1.93,3.43,2.73,4.75a49.89,49.89,0,0,1,6.07,23.5,47.9,47.9,0,0,1-3,15.57,20.58,20.58,0,0,0-1,6.69,27.73,27.73,0,0,0,1.32,7.57A71.17,71.17,0,0,0,742.78,883.52Z" transform="translate(-45 -62.4)"></path><path d="M773.86,843.13q7.3-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q767.09,848.06,773.86,843.13Z" transform="translate(-45 -62.4)"></path><path d="M797.51,774.66q-7.39-14.34-17.51-17.95V773c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q804.11,787.51,797.51,774.66Z" transform="translate(-45 -62.4)"></path><polygon points="733 697.1 730 697.1 730 815.35 736 815.35 736 697.1 730 697.1 733 697.1 730 697.1 730 815.35 736 815.35 736 697.1 730 697.1 733 697.1"></polygon><path d="M943.66,845.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.56,8.56,0,0,1-1.4-5.28q0-3.26,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1.05,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,943.66,845.2Zm.8-13.73q.62-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,944.46,831.47Z" transform="translate(-45 -62.4)"></path><polygon points="989.25 829.1 989.25 830.1 1019.75 830.1 1019.75 828.1 989.25 828.1 989.25 830.1 989.25 829.1 989.25 830.1 1019.75 830.1 1019.75 828.1 989.25 828.1 989.25 830.1 989.25 829.1"></polygon><path d="M1060.16,910.45a2.7,2.7,0,0,0-.35-1.59c-1.11,0-2.79-.76-5-2.28s-3.34-2.82-3.34-3.88q0-.16,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.6,5.8-5.37,8.8-2.73-4.12-2.72-12.67a5.27,5.27,0,0,0-.18-2q-1.41,0-2.9,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.66,8.7-4.67,11c0,1.34.38,2,1.15,2q.87,0,2.81-3.7c1.41-2.7,2.35-4.16,2.82-4.4,1.88,1.76,3.25,3.08,4.13,4q2.57,2.39,4.84,2.38Q1060.16,916.52,1060.16,910.45Z" transform="translate(-45 -62.4)"></path><path d="M1076.51,843.41q-7.4-14.34-17.51-17.95v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1083.11,856.26,1076.51,843.41Z" transform="translate(-45 -62.4)"></path><path d="M1076.51,821.41q-7.4-14.34-17.51-17.95v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1083.11,834.26,1076.51,821.41Z" transform="translate(-45 -62.4)"></path><polygon points="1012 741.1 1009 741.1 1009 829.1 1015 829.1 1015 741.1 1009 741.1 1012 741.1 1009 741.1 1009 829.1 1015 829.1 1015 741.1 1009 741.1 1012 741.1"></polygon><path d="M1182.66,845.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.62,8.62,0,0,1-1.4-5.28q0-3.26,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,1182.66,845.2Zm.8-13.73q.61-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,1183.46,831.47Z" transform="translate(-45 -62.4)"></path><polygon points="1228.25 829.1 1228.25 830.1 1258.75 830.1 1258.75 828.1 1228.25 828.1 1228.25 830.1 1228.25 829.1 1228.25 830.1 1258.75 830.1 1258.75 828.1 1228.25 828.1 1228.25 830.1 1228.25 829.1"></polygon><path d="M1299.16,910.45a2.7,2.7,0,0,0-.35-1.59c-1.11,0-2.79-.76-5-2.28s-3.34-2.82-3.34-3.88q0-.16,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.6,5.8-5.37,8.8-2.72-4.12-2.72-12.67a5.27,5.27,0,0,0-.18-2q-1.41,0-2.9,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.66,8.7-4.67,11c0,1.34.38,2,1.15,2q.87,0,2.81-3.7c1.41-2.7,2.35-4.16,2.82-4.4,1.88,1.76,3.25,3.08,4.13,4q2.57,2.39,4.84,2.38Q1299.16,916.52,1299.16,910.45Z" transform="translate(-45 -62.4)"></path><polygon points="1251 737.1 1248 737.1 1248 829.1 1254 829.1 1254 737.1 1248 737.1 1251 737.1 1248 737.1 1248 829.1 1254 829.1 1254 737.1 1248 737.1 1251 737.1"></polygon><polygon points="1359.75 829.1 1359.75 830.1 1405.25 830.1 1405.25 828.1 1359.75 828.1 1359.75 830.1 1359.75 829.1 1359.75 830.1 1405.25 830.1 1405.25 828.1 1359.75 828.1 1359.75 830.1 1359.75 829.1"></polygon><polygon points="1359.75 851.1 1359.75 852.1 1405.25 852.1 1405.25 850.1 1359.75 850.1 1359.75 852.1 1359.75 851.1 1359.75 852.1 1405.25 852.1 1405.25 850.1 1359.75 850.1 1359.75 852.1 1359.75 851.1"></polygon><path d="M1435.86,920.13q7.31-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q1429.08,925.06,1435.86,920.13Z" transform="translate(-45 -62.4)"></path><path d="M1397.11,906.31q0,8.1-9.24,16.63l.27-10c0-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S1397.11,904.61,1397.11,906.31Zm-8.27-6.77.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7-11.61,7-20.68,0-8.72-5.1-8.71Q1392.8,892.76,1388.84,899.54Z" transform="translate(-45 -62.4)"></path><path d="M1435.86,887.13q7.31-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q1429.08,892.06,1435.86,887.13Z" transform="translate(-45 -62.4)"></path><polyline points="1253.5 753.6 1397.5 741.87 1397.5 753.87 1253.5 765.6"></polyline><polyline points="1253.5 736.1 1397.5 724.37 1397.5 736.37 1253.5 748.1"></polyline><polygon points="1395 725.37 1392 725.37 1392 848.35 1398 848.35 1398 725.37 1392 725.37 1395 725.37 1392 725.37 1392 848.35 1398 848.35 1398 725.37 1392 725.37 1395 725.37"></polygon><path d="M1544.66,845.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.62,8.62,0,0,1-1.4-5.28q0-3.26,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,1544.66,845.2Zm.8-13.73q.61-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,1545.46,831.47Z" transform="translate(-45 -62.4)"></path><polygon points="1609.75 829.1 1609.75 830.1 1655.25 830.1 1655.25 828.1 1609.75 828.1 1609.75 830.1 1609.75 829.1 1609.75 830.1 1655.25 830.1 1655.25 828.1 1609.75 828.1 1609.75 830.1 1609.75 829.1"></polygon><polygon points="1609.75 851.1 1609.75 852.1 1655.25 852.1 1655.25 850.1 1609.75 850.1 1609.75 852.1 1609.75 851.1 1609.75 852.1 1655.25 852.1 1655.25 850.1 1609.75 850.1 1609.75 852.1 1609.75 851.1"></polygon><path d="M1685.86,931.13q7.31-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q1679.08,936.06,1685.86,931.13Z" transform="translate(-45 -62.4)"></path><path d="M1685.86,887.13q7.31-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q1679.08,892.06,1685.86,887.13Z" transform="translate(-45 -62.4)"></path><path d="M1647.11,873.31q0,8.1-9.24,16.63l.27-10c0-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S1647.11,871.61,1647.11,873.31Zm-8.27-6.77.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7-11.61,7-20.68,0-8.72-5.1-8.71Q1642.8,859.76,1638.84,866.54Z" transform="translate(-45 -62.4)"></path><polygon points="1645 725.6 1642 725.6 1642 859.35 1648 859.35 1648 725.6 1642 725.6 1645 725.6 1642 725.6 1642 859.35 1648 859.35 1648 725.6 1642 725.6 1645 725.6"></polygon><polygon points="1755.75 829.1 1755.75 830.1 1801.25 830.1 1801.25 828.1 1755.75 828.1 1755.75 830.1 1755.75 829.1 1755.75 830.1 1801.25 830.1 1801.25 828.1 1755.75 828.1 1755.75 830.1 1755.75 829.1"></polygon><polygon points="1755.75 851.1 1755.75 852.1 1801.25 852.1 1801.25 850.1 1755.75 850.1 1755.75 852.1 1755.75 851.1 1755.75 852.1 1801.25 852.1 1801.25 850.1 1755.75 850.1 1755.75 852.1 1755.75 851.1"></polygon><path d="M1831.86,920.13q7.31-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q1825.08,925.06,1831.86,920.13Z" transform="translate(-45 -62.4)"></path><path d="M1831.86,887.13q7.31-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q1825.08,892.06,1831.86,887.13Z" transform="translate(-45 -62.4)"></path><path d="M1792.32,881.76a12.67,12.67,0,0,1-7.83,3.87l.35-6.69a15.54,15.54,0,0,1,7.92-4.75C1792.64,878.18,1792.5,880.7,1792.32,881.76ZM1791,862.05q-4.66,1.94-5.63,3.61l1.32-34.76h-1.41a5,5,0,0,0-2.11.79l-3.61,66.7a6.23,6.23,0,0,0,2.46.53,12.94,12.94,0,0,0,5.94-2c2.38-1.32,3.89-2.66,4.54-4l-2,37.75q-.09.09,1.32.09a6,6,0,0,0,2.29-1.32l3.61-69.26A19.28,19.28,0,0,0,1791,862.05Z" transform="translate(-45 -62.4)"></path><rect x="1647" y="740.6" width="147" height="12"></rect><rect x="1647" y="724.1" width="147" height="12"></rect><polygon points="1791 725.6 1788 725.6 1788 848.35 1794 848.35 1794 725.6 1788 725.6 1791 725.6 1788 725.6 1788 848.35 1794 848.35 1794 725.6 1788 725.6 1791 725.6"></polygon><path d="M1677,946q27.49,26.13,78.5,20.62T1823,935q-15.81,17-66.81,22.5T1677,946Z" transform="translate(-45 -62.4)"></path><path d="M1940.66,845.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.62,8.62,0,0,1-1.4-5.28q0-3.26,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,1940.66,845.2Zm.8-13.73q.61-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,1941.46,831.47Z" transform="translate(-45 -62.4)"></path><polygon points="1989.25 829.1 1989.25 830.1 2019.75 830.1 2019.75 828.1 1989.25 828.1 1989.25 830.1 1989.25 829.1 1989.25 830.1 2019.75 830.1 2019.75 828.1 1989.25 828.1 1989.25 830.1 1989.25 829.1"></polygon><polygon points="1989.25 851.1 1989.25 852.1 2019.75 852.1 2019.75 850.1 1989.25 850.1 1989.25 852.1 1989.25 851.1 1989.25 852.1 2019.75 852.1 2019.75 850.1 1989.25 850.1 1989.25 852.1 1989.25 851.1"></polygon><path d="M2060.16,921.45a2.7,2.7,0,0,0-.35-1.59c-1.11,0-2.79-.76-5-2.28s-3.34-2.82-3.34-3.88q0-.16,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.6,5.8-5.37,8.8-2.72-4.12-2.72-12.67a5.27,5.27,0,0,0-.18-2q-1.41,0-2.9,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.66,8.7-4.67,11c0,1.34.38,2,1.15,2q.87,0,2.81-3.7c1.41-2.7,2.35-4.16,2.82-4.4,1.88,1.76,3.25,3.08,4.13,4q2.57,2.39,4.84,2.38Q2060.16,927.52,2060.16,921.45Z" transform="translate(-45 -62.4)"></path><path d="M2076.51,843.41q-7.4-14.34-17.51-17.95v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q2083.11,856.26,2076.51,843.41Z" transform="translate(-45 -62.4)"></path><path d="M2076.51,821.41q-7.4-14.34-17.51-17.95v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q2083.11,834.26,2076.51,821.41Z" transform="translate(-45 -62.4)"></path><polygon points="2012 741.1 2009 741.1 2009 840.1 2015 840.1 2015 741.1 2009 741.1 2012 741.1 2009 741.1 2009 840.1 2015 840.1 2015 741.1 2009 741.1 2012 741.1"></polygon><path d="M270.26,1089.13q7.31-5.37,7.31-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.69,11.44a5.74,5.74,0,0,0,3.26,5.37,13.15,13.15,0,0,0,6.42,1.41Q263.49,1094.06,270.26,1089.13Z" transform="translate(-45 -62.4)"></path><path d="M291.13,1079.4a5.21,5.21,0,0,0,5.28-5.54c-.09-3.26-2.12-5.55-5.2-5.55a5.26,5.26,0,0,0-5.28,5.55A5.15,5.15,0,0,0,291.13,1079.4Z" transform="translate(-45 -62.4)"></path><polygon points="206 1022.85 203 1022.85 203 1114.1 209 1114.1 209 1022.85 203 1022.85 206 1022.85 203 1022.85 203 1114.1 209 1114.1 209 1022.85 203 1022.85 206 1022.85"></polygon><path d="M616.8,1024.45a2.7,2.7,0,0,0-.35-1.59q-1.68,0-5-2.28t-3.34-3.88q0-.16,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.61,5.8-5.37,8.8-2.73-4.12-2.73-12.67a5.65,5.65,0,0,0-.17-2q-1.41,0-2.91,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.67,8.7-4.66,11c0,1.34.38,2,1.14,2q.89,0,2.82-3.7c1.41-2.7,2.35-4.16,2.82-4.4q2.81,2.64,4.13,4c1.7,1.59,3.32,2.38,4.84,2.38Q616.8,1030.52,616.8,1024.45Z" transform="translate(-45 -62.4)"></path><path d="M581.11,1009.31q0,8.1-9.24,16.63l.27-10c.05-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S581.11,1007.61,581.11,1009.31Zm-8.27-6.77.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7.05-11.61,7-20.68,0-8.72-5.1-8.71Q576.8,995.76,572.84,1002.54Z" transform="translate(-45 -62.4)"></path><polyline points="523.5 1055.34 552.5 1052.6 552.5 1064.6 523.5 1067.34"></polyline><polyline points="203.5 1103.1 552.5 1070.1 552.5 1082.1 203.5 1115.1"></polyline><polygon points="555 965.1 552 965.1 552 1081.1 558 1081.1 558 965.1 552 965.1 555 965.1 552 965.1 552 1081.1 558 1081.1 558 965.1 552 965.1 555 965.1"></polygon><path d="M762.93,1071.75a70,70,0,0,1,6.42-11.53,100.29,100.29,0,0,0,6.69-11.44,5.12,5.12,0,0,0,.53-2.11c0-1.35-.65-2-1.94-2a4.07,4.07,0,0,0-1.49.27,30.31,30.31,0,0,1-6.78,5.1q-4.67,2.73-7.83,2.73c-2,0-3-.82-3-2.46a11.41,11.41,0,0,1,.88-3.61,11.77,11.77,0,0,0,.88-3.61.94.94,0,0,0-1.06-1.06c-1.88,0-3.78,2.06-5.72,6.16q-2.64,5.55-2.64,8.89,0,7.48,6.78,7.48a61.08,61.08,0,0,0,8.88-1.49q-8.17,16.89-8.18,19.18a9.54,9.54,0,0,0,.18,2.38c.23.7.85,1.05,1.84,1.05s2.2-1.07,4-3.21,2.64-3.68,2.64-4.62a3.6,3.6,0,0,0-.92-1.72,3.69,3.69,0,0,1-.92-2.24C762.49,1072.92,762.75,1072.22,762.93,1071.75Z" transform="translate(-45 -62.4)"></path><path d="M947.8,1024.45a2.7,2.7,0,0,0-.35-1.59q-1.68,0-5-2.28t-3.34-3.88q0-.16,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.61,5.8-5.37,8.8-2.73-4.12-2.73-12.67a5.65,5.65,0,0,0-.17-2q-1.41,0-2.91,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.67,8.7-4.66,11c0,1.34.38,2,1.14,2q.89,0,2.82-3.7c1.41-2.7,2.35-4.16,2.82-4.4q2.81,2.64,4.13,4c1.7,1.59,3.32,2.38,4.84,2.38Q947.8,1030.52,947.8,1024.45Z" transform="translate(-45 -62.4)"></path><path d="M955.54,1029.53a1.52,1.52,0,0,0,.09.62,48.46,48.46,0,0,1,1.5,8.27q.35,6.15.35,5.37a43.75,43.75,0,0,1-2.46,13.28q-1.68,5.12-8.63,11.88-8.1,7.58-12.23,11.27-6.17,5.8-6.16,8.53v13.73q12-5.1,20.42-20.5,12-21.74,12-38.19a61.13,61.13,0,0,0-2-16C956.48,1027.59,955.54,1028.18,955.54,1029.53Z" transform="translate(-45 -62.4)"></path><path d="M955.54,1051.53a1.52,1.52,0,0,0,.09.62,48.46,48.46,0,0,1,1.5,8.27q.35,6.15.35,5.37a43.75,43.75,0,0,1-2.46,13.28q-1.68,5.12-8.63,11.88-8.1,7.58-12.23,11.27-6.17,5.8-6.16,8.53v13.73q12-5.1,20.42-20.5,12-21.74,12-38.19a61.13,61.13,0,0,0-2-16C956.48,1049.59,955.54,1050.18,955.54,1051.53Z" transform="translate(-45 -62.4)"></path><polygon points="886 965.1 883 965.1 883 1053.1 889 1053.1 889 965.1 883 965.1 886 965.1 883 965.1 883 1053.1 889 1053.1 889 965.1 883 965.1 886 965.1"></polygon><path d="M1055.66,1080.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.62,8.62,0,0,1-1.4-5.28q0-3.25,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,1055.66,1080.2Zm.8-13.73q.61-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,1056.46,1066.47Z" transform="translate(-45 -62.4)"></path><path d="M1182.93,1071.75a70,70,0,0,1,6.42-11.53,100.29,100.29,0,0,0,6.69-11.44,5.12,5.12,0,0,0,.53-2.11c0-1.35-.65-2-1.94-2a4.07,4.07,0,0,0-1.49.27,30.31,30.31,0,0,1-6.78,5.1q-4.66,2.73-7.83,2.73c-2,0-3-.82-3-2.46a11.41,11.41,0,0,1,.88-3.61,11.77,11.77,0,0,0,.88-3.61.94.94,0,0,0-1.06-1.06c-1.88,0-3.78,2.06-5.72,6.16q-2.64,5.55-2.64,8.89,0,7.48,6.78,7.48a61.08,61.08,0,0,0,8.88-1.49q-8.18,16.89-8.18,19.18a9.54,9.54,0,0,0,.18,2.38c.23.7.85,1.05,1.84,1.05s2.2-1.07,4-3.21,2.64-3.68,2.64-4.62a3.6,3.6,0,0,0-.92-1.72,3.69,3.69,0,0,1-.92-2.24C1182.49,1072.92,1182.75,1072.22,1182.93,1071.75Z" transform="translate(-45 -62.4)"></path><path d="M1212.13,1055.9a5.21,5.21,0,0,0,5.28-5.54c-.09-3.26-2.12-5.55-5.2-5.55a5.26,5.26,0,0,0-5.28,5.55A5.15,5.15,0,0,0,1212.13,1055.9Z" transform="translate(-45 -62.4)"></path><path d="M1549.16,1090.45a2.7,2.7,0,0,0-.35-1.59c-1.11,0-2.79-.76-5-2.28s-3.34-2.82-3.34-3.88q0-.17,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.6,5.8-5.37,8.8-2.72-4.12-2.72-12.67a5.27,5.27,0,0,0-.18-2q-1.41,0-2.9,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.66,8.7-4.67,11c0,1.34.38,2,1.15,2q.87,0,2.81-3.7c1.41-2.7,2.35-4.16,2.82-4.4,1.88,1.76,3.25,3.08,4.13,4q2.57,2.38,4.84,2.38Q1549.16,1096.52,1549.16,1090.45Z" transform="translate(-45 -62.4)"></path><path d="M1565.51,1023.41q-7.4-14.34-17.51-17.95v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1572.11,1036.26,1565.51,1023.41Z" transform="translate(-45 -62.4)"></path><path d="M1565.51,1001.41q-7.4-14.34-17.51-17.95v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1572.11,1014.26,1565.51,1001.41Z" transform="translate(-45 -62.4)"></path><polygon points="1501 921.1 1498 921.1 1498 1009.1 1504 1009.1 1504 921.1 1498 921.1 1501 921.1 1498 921.1 1498 1009.1 1504 1009.1 1504 921.1 1498 921.1 1501 921.1"></polygon><path d="M1678.93,1071.75a70,70,0,0,1,6.42-11.53,100.29,100.29,0,0,0,6.69-11.44,5.12,5.12,0,0,0,.53-2.11c0-1.35-.65-2-1.94-2a4.07,4.07,0,0,0-1.49.27,30.31,30.31,0,0,1-6.78,5.1q-4.66,2.73-7.83,2.73c-2,0-3-.82-3-2.46a11.41,11.41,0,0,1,.88-3.61,11.77,11.77,0,0,0,.88-3.61.94.94,0,0,0-1.06-1.06c-1.88,0-3.78,2.06-5.72,6.16q-2.64,5.55-2.64,8.89,0,7.48,6.78,7.48a61.08,61.08,0,0,0,8.88-1.49q-8.18,16.89-8.18,19.18a9.54,9.54,0,0,0,.18,2.38c.23.7.85,1.05,1.84,1.05s2.2-1.07,4-3.21,2.64-3.68,2.64-4.62a3.6,3.6,0,0,0-.92-1.72,3.69,3.69,0,0,1-.92-2.24C1678.49,1072.92,1678.75,1072.22,1678.93,1071.75Z" transform="translate(-45 -62.4)"></path><path d="M1945.16,1090.45a2.7,2.7,0,0,0-.35-1.59c-1.11,0-2.79-.76-5-2.28s-3.34-2.82-3.34-3.88q0-.17,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.6,5.8-5.37,8.8-2.72-4.12-2.72-12.67a5.27,5.27,0,0,0-.18-2q-1.41,0-2.9,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.66,8.7-4.67,11c0,1.34.38,2,1.15,2q.87,0,2.81-3.7c1.41-2.7,2.35-4.16,2.82-4.4,1.88,1.76,3.25,3.08,4.13,4q2.57,2.38,4.84,2.38Q1945.16,1096.52,1945.16,1090.45Z" transform="translate(-45 -62.4)"></path><path d="M1961.51,1023.41q-7.4-14.34-17.51-17.95v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1968.11,1036.26,1961.51,1023.41Z" transform="translate(-45 -62.4)"></path><path d="M1961.51,1001.41q-7.4-14.34-17.51-17.95v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1968.11,1014.26,1961.51,1001.41Z" transform="translate(-45 -62.4)"></path><polygon points="1897 921.1 1894 921.1 1894 1009.1 1900 1009.1 1900 921.1 1894 921.1 1897 921.1 1894 921.1 1894 1009.1 1900 1009.1 1900 921.1 1894 921.1 1897 921.1"></polygon><path d="M2055.66,1080.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.62,8.62,0,0,1-1.4-5.28q0-3.25,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1.05,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,2055.66,1080.2Zm.8-13.73q.61-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,2056.46,1066.47Z" transform="translate(-45 -62.4)"></path></g><g class="bar bar-0"><path d="M319.66,281.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.56,8.56,0,0,1-1.4-5.28q0-3.25,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1.05,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,319.66,281.2Zm.8-13.73q.62-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,320.46,267.47Z" transform="translate(-45 -62.4)"></path><path d="M299.36,105a4,4,0,0,1,1.25,3.08,6.23,6.23,0,0,1-.41,2q-2.49,7-7.54,7a3.33,3.33,0,0,1-.46-.06h-.53a12.43,12.43,0,0,1-.92-4.47c-.08-1.81-.14-3.4-.18-4.75a5.34,5.34,0,0,1,2.18-2.81,6,6,0,0,1,3.45-1.08A4.48,4.48,0,0,1,299.36,105Zm-3.05-15a17.7,17.7,0,0,1-6.55,7.54q-.81-4.07-.81-4.29c0-1.24.69-2.7,2.09-4.38s2.68-2.52,3.88-2.52a1.79,1.79,0,0,1,1.34.63,2.07,2.07,0,0,1,.58,1.4A4.42,4.42,0,0,1,296.31,89.94Zm7.31,10.5A5.44,5.44,0,0,0,299,98a19.36,19.36,0,0,0-2.55.23,13.23,13.23,0,0,0,5.8-11.54c0-.46,0-.94-.06-1.45q-.41-5.16-6.67-5.16-4.58,0-9.28,5.39t-4.7,10q0,3.88,2.15,3.88a6.74,6.74,0,0,0,2.44-.52q0,3,.87,12.07-2.5,1.22-2.5,5.33c0,2.36.47,4,1.39,5.08s2.56,1.53,4.88,1.53a14.59,14.59,0,0,0,10.7-4.23A14.42,14.42,0,0,0,305.71,108a17.77,17.77,0,0,0-.17-2.38A12,12,0,0,0,303.62,100.44Z" transform="translate(-45 -62.4)"></path><path d="M319.58,97q0,5.52-6.3,11.34l.18-6.84a9.1,9.1,0,0,1,1.77-3.69q1.71-2.55,3-2.55T319.58,97Zm-5.64-4.62.3-24.36a1.25,1.25,0,0,0-.48-.06,3.56,3.56,0,0,0-2.7,1.5l-.54,50.1a62.89,62.89,0,0,0,9.36-11.76q4.8-7.92,4.8-14.1,0-5.94-3.48-5.94Q316.64,87.8,313.94,92.42Z" transform="translate(-45 -62.4)"></path><path d="M335.35,110.94q1.15-5.34,1.68-7.37,2.73-11.77,3.71-19.6a1.49,1.49,0,0,1,.06-.35v-.29q0-3.94-6-4a26.84,26.84,0,0,0-5.57.64,11.38,11.38,0,0,0-5.1,2.09v8.12a30.77,30.77,0,0,1,9.45-2.09q.81,0,2.55.12c.12,0,.18.15.18.35a.33.33,0,0,1-.06.23,77.25,77.25,0,0,0-2.76,10.5,66.88,66.88,0,0,0-1.77,10.38,2.43,2.43,0,0,0,.53,1.68,9.13,9.13,0,0,0,1.45,1Q335.17,111.87,335.35,110.94Z" transform="translate(-45 -62.4)"></path><polygon points="364.25 265.1 364.25 266.1 394.75 266.1 394.75 264.1 364.25 264.1 364.25 266.1 364.25 265.1 364.25 266.1 394.75 266.1 394.75 264.1 364.25 264.1 364.25 266.1 364.25 265.1"></polygon><path d="M435.16,346.45a2.7,2.7,0,0,0-.35-1.59q-1.68,0-5-2.28t-3.34-3.88q0-.17,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.6,5.8-5.37,8.8-2.73-4.12-2.72-12.67a5.27,5.27,0,0,0-.18-2q-1.41,0-2.9,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.66,8.7-4.67,11c0,1.34.38,2,1.15,2q.87,0,2.81-3.7c1.41-2.7,2.35-4.16,2.82-4.4q2.81,2.64,4.13,4c1.7,1.59,3.32,2.38,4.84,2.38Q435.16,352.52,435.16,346.45Z" transform="translate(-45 -62.4)"></path><polygon points="387 151.1 384 151.1 384 265.1 390 265.1 390 151.1 384 151.1 387 151.1 384 151.1 384 265.1 390 265.1 390 151.1 384 151.1 387 151.1"></polygon><path d="M571.86,312.13q7.3-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q565.09,317.06,571.86,312.13Z" transform="translate(-45 -62.4)"></path><path d="M571.86,279.13q7.3-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q565.09,284.06,571.86,279.13Z" transform="translate(-45 -62.4)"></path><path d="M533.11,265.31q0,8.1-9.24,16.63l.27-10c.05-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S533.11,263.61,533.11,265.31Zm-8.27-6.77.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7.05-11.61,7-20.68,0-8.72-5.1-8.71Q528.8,251.76,524.84,258.54Z" transform="translate(-45 -62.4)"></path><polyline points="389.5 167.6 533.5 132.4 533.5 144.4 389.5 179.6"></polyline><polyline points="389.5 150.1 533.5 114.9 533.5 126.9 389.5 162.1"></polyline><polygon points="531 115.9 528 115.9 528 240.35 534 240.35 534 115.9 528 115.9 531 115.9 528 115.9 528 240.35 534 240.35 534 115.9 528 115.9 531 115.9"></polygon><path d="M667.66,281.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.56,8.56,0,0,1-1.4-5.28q0-3.25,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1.05,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,667.66,281.2Zm.8-13.73q.62-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,668.46,267.47Z" transform="translate(-45 -62.4)"></path><path d="M755,270.35q-5.71-11.07-13.53-13.87v12.58l4.42,3.4a34.41,34.41,0,0,1,7.75,7.62,17.77,17.77,0,0,1,3.2,10.2q0,5.91-2.18,8.63a3,3,0,0,0,2,.82q1.85,0,2.79-4.35a27.8,27.8,0,0,0,.68-6Q760.13,280.28,755,270.35Z" transform="translate(-45 -62.4)"></path><polygon points="694.5 196.35 691.5 196.35 691.5 251.35 697.5 251.35 697.5 196.35 691.5 196.35 694.5 196.35 691.5 196.35 691.5 251.35 697.5 251.35 697.5 196.35 691.5 196.35 694.5 196.35"></polygon><polygon points="675 236.23 676.34 238.91 720.34 216.91 717.66 211.54 673.66 233.54 676.34 238.91 675 236.23 676.34 238.91 720.34 216.91 717.66 211.54 673.66 233.54 676.34 238.91 675 236.23"></polygon><path d="M749.38,324.25q2.7,11,8.12,11t8.12-11q-2.7,5.91-8.12,5.91T749.38,324.25Z" transform="translate(-45 -62.4)"></path><path d="M736.79,321.51q5.64-4.15,5.64-8.5,0-6.33-8.23-6.33-4.35,0-9.52,4.56t-5.16,8.84a4.43,4.43,0,0,0,2.51,4.15,10.27,10.27,0,0,0,5,1.09Q731.55,325.32,736.79,321.51Z" transform="translate(-45 -62.4)"></path><path d="M708.13,310.83q0,6.26-7.14,12.85l.21-7.75q.06-1.29,2-4.18t3.44-2.89C707.63,308.86,708.13,309.52,708.13,310.83Zm-6.39-5.23.34-27.61a1.3,1.3,0,0,0-.54-.07,4,4,0,0,0-3.06,1.7l-.62,56.78a71.09,71.09,0,0,0,10.61-13.33q5.44-9,5.44-16,0-6.73-3.94-6.73Q704.8,300.36,701.74,305.6Z" transform="translate(-45 -62.4)"></path><path d="M832.86,323.13q7.3-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q826.09,328.06,832.86,323.13Z" transform="translate(-45 -62.4)"></path><path d="M788.06,317.32q-2.38,2.28-7.75,3.17l.35-5.37a16,16,0,0,1,7.66-3.78Zm-9.16,34.5a14.62,14.62,0,0,0-1.14-6.52q-5.19-9.76-6.51-13.11a37.63,37.63,0,0,1-3-14.17q0-4.31,2.46-12.32t2.46-12a22.72,22.72,0,0,0-1.4-6.52A70.49,70.49,0,0,0,766,315.47q0,21.39,12.14,44.09A46.38,46.38,0,0,0,778.9,351.82Zm2.38-47.26,1.32-28a7.06,7.06,0,0,1-1-.09,7.06,7.06,0,0,0-1-.09,5.75,5.75,0,0,1-1,.79l-4,53.59a6.27,6.27,0,0,0,2.2.36,12.8,12.8,0,0,0,5.32-1.5,9.47,9.47,0,0,0,4.18-3.26l-1.85,30.28a13,13,0,0,1,1.85.26c-.12,0,.44-.41,1.67-1.23q2.38-33.62,3.44-55.62Q783.39,302.62,781.28,304.56Zm20.5,15a93.37,93.37,0,0,0-3-23.06A81.47,81.47,0,0,0,790.17,275a30.15,30.15,0,0,0-1,6.87,33.72,33.72,0,0,0,.35,3.78,14.61,14.61,0,0,0,.79,3.61q1.93,3.44,2.73,4.75a49.89,49.89,0,0,1,6.07,23.5,47.9,47.9,0,0,1-3,15.57,20.58,20.58,0,0,0-1,6.69,27.73,27.73,0,0,0,1.32,7.57A71.17,71.17,0,0,0,801.78,319.52Z" transform="translate(-45 -62.4)"></path><path d="M832.86,279.13q7.3-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q826.09,284.06,832.86,279.13Z" transform="translate(-45 -62.4)"></path><path d="M856.51,210.66q-7.39-14.34-17.51-17.95V209c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q863.11,223.51,856.51,210.66Z" transform="translate(-45 -62.4)"></path><polygon points="792 133.1 789 133.1 789 251.35 795 251.35 795 133.1 789 133.1 792 133.1 789 133.1 789 251.35 795 251.35 795 133.1 789 133.1 792 133.1"></polygon><path d="M1002.66,281.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.62,8.62,0,0,1-1.4-5.28q0-3.25,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1.05,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,1002.66,281.2Zm.8-13.73q.62-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,1003.46,267.47Z" transform="translate(-45 -62.4)"></path><polygon points="1047.25 265.1 1047.25 266.1 1077.75 266.1 1077.75 264.1 1047.25 264.1 1047.25 266.1 1047.25 265.1 1047.25 266.1 1077.75 266.1 1077.75 264.1 1047.25 264.1 1047.25 266.1 1047.25 265.1"></polygon><path d="M1118.16,346.45a2.7,2.7,0,0,0-.35-1.59c-1.11,0-2.79-.76-5-2.28s-3.34-2.82-3.34-3.88q0-.17,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.6,5.8-5.37,8.8-2.72-4.12-2.72-12.67a5.27,5.27,0,0,0-.18-2q-1.41,0-2.9,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.66,8.7-4.67,11c0,1.34.38,2,1.15,2q.87,0,2.81-3.7c1.41-2.7,2.35-4.16,2.82-4.4,1.88,1.76,3.25,3.08,4.13,4q2.57,2.39,4.84,2.38Q1118.16,352.52,1118.16,346.45Z" transform="translate(-45 -62.4)"></path><path d="M1134.51,279.41q-7.4-14.34-17.51-18v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1141.11,292.26,1134.51,279.41Z" transform="translate(-45 -62.4)"></path><path d="M1134.51,257.41q-7.4-14.34-17.51-18v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1141.11,270.26,1134.51,257.41Z" transform="translate(-45 -62.4)"></path><polygon points="1070 177.1 1067 177.1 1067 265.1 1073 265.1 1073 177.1 1067 177.1 1070 177.1 1067 177.1 1067 265.1 1073 265.1 1073 177.1 1067 177.1 1070 177.1"></polygon><path d="M1256.86,312.13q7.31-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q1250.08,317.06,1256.86,312.13Z" transform="translate(-45 -62.4)"></path><path d="M1256.86,279.13q7.31-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q1250.08,284.06,1256.86,279.13Z" transform="translate(-45 -62.4)"></path><path d="M1280.51,213.41q-7.4-14.34-17.51-17.95v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1287.11,226.26,1280.51,213.41Z" transform="translate(-45 -62.4)"></path><path d="M1280.51,191.41q-7.4-14.34-17.51-17.95v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1287.11,204.26,1280.51,191.41Z" transform="translate(-45 -62.4)"></path><polygon points="1216 111.1 1213 111.1 1213 240.35 1219 240.35 1219 111.1 1213 111.1 1216 111.1 1213 111.1 1213 240.35 1219 240.35 1219 111.1 1213 111.1 1216 111.1"></polygon><path d="M1372.66,281.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.62,8.62,0,0,1-1.4-5.28q0-3.25,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,1372.66,281.2Zm.8-13.73q.61-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,1373.46,267.47Z" transform="translate(-45 -62.4)"></path><polygon points="1421.25 265.1 1421.25 266.1 1451.75 266.1 1451.75 264.1 1421.25 264.1 1421.25 266.1 1421.25 265.1 1421.25 266.1 1451.75 266.1 1451.75 264.1 1421.25 264.1 1421.25 266.1 1421.25 265.1"></polygon><path d="M1492.16,346.45a2.7,2.7,0,0,0-.35-1.59c-1.11,0-2.79-.76-5-2.28s-3.34-2.82-3.34-3.88q0-.17,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.6,5.8-5.37,8.8-2.72-4.12-2.72-12.67a5.27,5.27,0,0,0-.18-2q-1.41,0-2.9,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.66,8.7-4.67,11c0,1.34.38,2,1.15,2q.87,0,2.81-3.7c1.41-2.7,2.35-4.16,2.82-4.4,1.88,1.76,3.25,3.08,4.13,4q2.57,2.39,4.84,2.38Q1492.16,352.52,1492.16,346.45Z" transform="translate(-45 -62.4)"></path><path d="M1508.51,279.41q-7.4-14.34-17.51-18v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1515.11,292.26,1508.51,279.41Z" transform="translate(-45 -62.4)"></path><path d="M1508.51,257.41q-7.4-14.34-17.51-18v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1515.11,270.26,1508.51,257.41Z" transform="translate(-45 -62.4)"></path><polygon points="1444 177.1 1441 177.1 1441 265.1 1447 265.1 1447 177.1 1441 177.1 1444 177.1 1441 177.1 1441 265.1 1447 265.1 1447 177.1 1441 177.1 1444 177.1"></polygon><path d="M1604,270.35q-5.71-11.07-13.53-13.87v12.58l4.42,3.4a34.41,34.41,0,0,1,7.75,7.62,17.77,17.77,0,0,1,3.2,10.2q0,5.91-2.18,8.63a3,3,0,0,0,2,.82q1.85,0,2.79-4.35a27.8,27.8,0,0,0,.68-6Q1609.13,280.28,1604,270.35Z" transform="translate(-45 -62.4)"></path><polygon points="1543.5 196.35 1540.5 196.35 1540.5 251.35 1546.5 251.35 1546.5 196.35 1540.5 196.35 1543.5 196.35 1540.5 196.35 1540.5 251.35 1546.5 251.35 1546.5 196.35 1540.5 196.35 1543.5 196.35"></polygon><polygon points="1524 236.23 1525.34 238.91 1569.34 216.91 1566.66 211.54 1522.66 233.54 1525.34 238.91 1524 236.23 1525.34 238.91 1569.34 216.91 1566.66 211.54 1522.66 233.54 1525.34 238.91 1524 236.23"></polygon><path d="M1598.38,324.25q2.7,11,8.12,11t8.12-11q-2.7,5.91-8.12,5.91T1598.38,324.25Z" transform="translate(-45 -62.4)"></path><path d="M1585.79,321.51q5.64-4.15,5.64-8.5,0-6.33-8.23-6.33-4.35,0-9.52,4.56t-5.16,8.84a4.43,4.43,0,0,0,2.51,4.15,10.27,10.27,0,0,0,5,1.09Q1580.55,325.32,1585.79,321.51Z" transform="translate(-45 -62.4)"></path><path d="M1557.13,310.83q0,6.26-7.14,12.85l.21-7.75q.06-1.29,2-4.18t3.44-2.89C1556.63,308.86,1557.13,309.52,1557.13,310.83Zm-6.39-5.23.34-27.61a1.3,1.3,0,0,0-.54-.07,4,4,0,0,0-3.06,1.7l-.62,56.78a71.09,71.09,0,0,0,10.61-13.33q5.44-9,5.44-16,0-6.73-3.94-6.73Q1553.8,300.36,1550.74,305.6Z" transform="translate(-45 -62.4)"></path><path d="M1682.86,323.13q7.31-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q1676.08,328.06,1682.86,323.13Z" transform="translate(-45 -62.4)"></path><path d="M1638.06,317.32q-2.38,2.28-7.75,3.17l.35-5.37a16,16,0,0,1,7.66-3.78Zm-9.16,34.5a14.62,14.62,0,0,0-1.14-6.52q-5.19-9.76-6.51-13.11a37.63,37.63,0,0,1-3-14.17q0-4.31,2.46-12.32t2.46-12a22.72,22.72,0,0,0-1.4-6.52,70.49,70.49,0,0,0-5.81,28.25q0,21.39,12.14,44.09A46.38,46.38,0,0,0,1628.9,351.82Zm2.38-47.26,1.32-28a7.2,7.2,0,0,1-1-.09,7.06,7.06,0,0,0-1-.09,5.75,5.75,0,0,1-1.05.79l-4,53.59a6.27,6.27,0,0,0,2.2.36,12.8,12.8,0,0,0,5.32-1.5,9.47,9.47,0,0,0,4.18-3.26l-1.85,30.28a13,13,0,0,1,1.85.26c-.12,0,.44-.41,1.67-1.23q2.38-33.62,3.44-55.62Q1633.39,302.62,1631.28,304.56Zm20.5,15a93.37,93.37,0,0,0-3-23.06,81.47,81.47,0,0,0-8.62-21.47,30.15,30.15,0,0,0-1,6.87,33.72,33.72,0,0,0,.35,3.78,14.61,14.61,0,0,0,.79,3.61q1.94,3.44,2.73,4.75a49.89,49.89,0,0,1,6.07,23.5,47.9,47.9,0,0,1-3,15.57,20.58,20.58,0,0,0-1.05,6.69,27.73,27.73,0,0,0,1.32,7.57A71.17,71.17,0,0,0,1651.78,319.52Z" transform="translate(-45 -62.4)"></path><path d="M1682.86,279.13q7.31-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q1676.08,284.06,1682.86,279.13Z" transform="translate(-45 -62.4)"></path><polygon points="1642 133.1 1639 133.1 1639 251.35 1645 251.35 1645 133.1 1639 133.1 1642 133.1 1639 133.1 1639 251.35 1645 251.35 1645 133.1 1639 133.1 1642 133.1"></polygon><polygon points="1974.75 265.1 1974.75 266.1 2020.25 266.1 2020.25 264.1 1974.75 264.1 1974.75 266.1 1974.75 265.1 1974.75 266.1 2020.25 266.1 2020.25 264.1 1974.75 264.1 1974.75 266.1 1974.75 265.1"></polygon><path d="M2050.86,345.13q7.3-5.37,7.3-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.68,11.44a5.73,5.73,0,0,0,3.25,5.37,13.16,13.16,0,0,0,6.43,1.41Q2044.08,350.06,2050.86,345.13Z" transform="translate(-45 -62.4)"></path><path d="M2074.51,279.41q-7.4-14.34-17.51-18v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q2081.11,292.26,2074.51,279.41Z" transform="translate(-45 -62.4)"></path><path d="M2074.51,257.41q-7.4-14.34-17.51-18v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q2081.11,270.26,2074.51,257.41Z" transform="translate(-45 -62.4)"></path><polygon points="2010 177.1 2007 177.1 2007 273.35 2013 273.35 2013 177.1 2007 177.1 2010 177.1 2007 177.1 2007 273.35 2013 273.35 2013 177.1 2007 177.1 2010 177.1"></polygon><path d="M326.26,525.13q7.31-5.37,7.31-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.69,11.44a5.74,5.74,0,0,0,3.26,5.37,13.15,13.15,0,0,0,6.42,1.41Q319.49,530.06,326.26,525.13Z" transform="translate(-45 -62.4)"></path><path d="M347.13,515.4a5.21,5.21,0,0,0,5.28-5.54c-.09-3.26-2.12-5.55-5.2-5.55a5.26,5.26,0,0,0-5.28,5.55A5.15,5.15,0,0,0,347.13,515.4Z" transform="translate(-45 -62.4)"></path><polygon points="262 458.85 259 458.85 259 550.1 265 550.1 265 458.85 259 458.85 262 458.85 259 458.85 259 550.1 265 550.1 265 458.85 259 458.85 262 458.85"></polygon><path d="M671.8,460.45a2.7,2.7,0,0,0-.35-1.59q-1.68,0-5-2.28t-3.34-3.88q0-.17,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.61,5.8-5.37,8.8-2.73-4.12-2.73-12.67a5.65,5.65,0,0,0-.17-2q-1.41,0-2.91,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.67,8.7-4.66,11c0,1.34.38,2,1.14,2q.89,0,2.82-3.7c1.41-2.7,2.35-4.16,2.82-4.4q2.81,2.64,4.13,4c1.7,1.59,3.32,2.38,4.84,2.38Q671.8,466.52,671.8,460.45Z" transform="translate(-45 -62.4)"></path><path d="M636.11,445.31q0,8.1-9.24,16.63l.27-10c.05-1.11.92-2.92,2.59-5.41s3.15-3.74,4.45-3.74S636.11,443.61,636.11,445.31Zm-8.27-6.77.44-35.73a1.78,1.78,0,0,0-.7-.09,5.2,5.2,0,0,0-4,2.2l-.8,73.48a92.13,92.13,0,0,0,13.73-17.25q7.05-11.61,7-20.68,0-8.72-5.1-8.71Q631.8,431.76,627.84,438.54Z" transform="translate(-45 -62.4)"></path><polyline points="578.5 491.35 607.5 488.6 607.5 500.6 578.5 503.35"></polyline><polyline points="259.5 539.1 607.5 506.1 607.5 518.1 259.5 551.1"></polyline><polygon points="610 401.1 607 401.1 607 517.1 613 517.1 613 401.1 607 401.1 610 401.1 607 401.1 607 517.1 613 517.1 613 401.1 607 401.1 610 401.1"></polygon><path d="M817.93,507.75a70,70,0,0,1,6.42-11.53A100.29,100.29,0,0,0,831,484.78a5.12,5.12,0,0,0,.53-2.11c0-1.35-.65-2-1.94-2a4.07,4.07,0,0,0-1.49.27,30.31,30.31,0,0,1-6.78,5.1q-4.67,2.73-7.83,2.73c-2,0-3-.82-3-2.46a11.41,11.41,0,0,1,.88-3.61,11.77,11.77,0,0,0,.88-3.61.94.94,0,0,0-1.06-1.06c-1.88,0-3.78,2.06-5.72,6.16q-2.64,5.55-2.64,8.89,0,7.49,6.78,7.48a61.08,61.08,0,0,0,8.88-1.49q-8.17,16.89-8.18,19.18a9.54,9.54,0,0,0,.18,2.38c.23.7.85,1,1.84,1s2.2-1.07,4-3.21,2.64-3.68,2.64-4.62a3.6,3.6,0,0,0-.92-1.72,3.69,3.69,0,0,1-.92-2.24C817.49,508.92,817.75,508.22,817.93,507.75Z" transform="translate(-45 -62.4)"></path><path d="M1006.8,460.45a2.7,2.7,0,0,0-.35-1.59q-1.68,0-5-2.28t-3.34-3.88q0-.17,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.61,5.8-5.37,8.8-2.73-4.12-2.73-12.67a5.65,5.65,0,0,0-.17-2q-1.41,0-2.91,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.67,8.7-4.66,11c0,1.34.38,2,1.14,2q.89,0,2.82-3.7c1.41-2.7,2.35-4.16,2.82-4.4q2.81,2.64,4.13,4c1.7,1.59,3.32,2.38,4.84,2.38Q1006.8,466.52,1006.8,460.45Z" transform="translate(-45 -62.4)"></path><path d="M1014.54,465.53a1.52,1.52,0,0,0,.09.62,48.46,48.46,0,0,1,1.5,8.27q.35,6.15.35,5.37a43.75,43.75,0,0,1-2.46,13.28q-1.68,5.12-8.63,11.88-8.1,7.57-12.23,11.27Q987,522,987,524.75v13.73q12-5.1,20.42-20.5,12-21.74,12-38.19a61.13,61.13,0,0,0-2-16C1015.48,463.59,1014.54,464.18,1014.54,465.53Z" transform="translate(-45 -62.4)"></path><path d="M1014.54,487.53a1.52,1.52,0,0,0,.09.62,48.46,48.46,0,0,1,1.5,8.27q.35,6.15.35,5.37a43.75,43.75,0,0,1-2.46,13.28q-1.68,5.11-8.63,11.88-8.1,7.57-12.23,11.27Q987,544,987,546.75v13.73q12-5.1,20.42-20.5,12-21.74,12-38.19a61.13,61.13,0,0,0-2-16C1015.48,485.59,1014.54,486.18,1014.54,487.53Z" transform="translate(-45 -62.4)"></path><polygon points="945 401.1 942 401.1 942 489.1 948 489.1 948 401.1 942 401.1 945 401.1 942 401.1 942 489.1 948 489.1 948 401.1 942 401.1 945 401.1"></polygon><path d="M1113.66,516.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.62,8.62,0,0,1-1.4-5.28q0-3.26,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,1113.66,516.2Zm.8-13.73q.61-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,1114.46,502.47Z" transform="translate(-45 -62.4)"></path><path d="M1249.66,516.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.62,8.62,0,0,1-1.4-5.28q0-3.26,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,1249.66,516.2Zm.8-13.73q.61-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,1250.46,502.47Z" transform="translate(-45 -62.4)"></path><path d="M1377.16,526.45a2.7,2.7,0,0,0-.35-1.59c-1.11,0-2.79-.76-5-2.28s-3.34-2.82-3.34-3.88q0-.16,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13-3.6,5.8-5.37,8.8-2.72-4.12-2.72-12.67a5.27,5.27,0,0,0-.18-2q-1.41,0-2.9,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.66,8.7-4.67,11c0,1.34.38,2,1.15,2q.87,0,2.81-3.7c1.41-2.7,2.35-4.16,2.82-4.4,1.88,1.76,3.25,3.08,4.13,4q2.57,2.39,4.84,2.38Q1377.16,532.52,1377.16,526.45Z" transform="translate(-45 -62.4)"></path><path d="M1393.51,459.41q-7.4-14.34-17.51-18v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1400.11,472.26,1393.51,459.41Z" transform="translate(-45 -62.4)"></path><path d="M1393.51,437.41q-7.4-14.34-17.51-18v16.28c2.76,2.11,4.66,3.58,5.72,4.4a44.31,44.31,0,0,1,10,9.86,23,23,0,0,1,4.14,13.2q0,7.65-2.82,11.17a3.86,3.86,0,0,0,2.55,1.06q2.39,0,3.61-5.63a35.44,35.44,0,0,0,.88-7.75Q1400.11,450.26,1393.51,437.41Z" transform="translate(-45 -62.4)"></path><polygon points="1329 357.1 1326 357.1 1326 445.1 1332 445.1 1332 357.1 1326 357.1 1329 357.1 1326 357.1 1326 445.1 1332 445.1 1332 357.1 1326 357.1 1329 357.1"></polygon><path d="M1487.93,507.75a70,70,0,0,1,6.42-11.53,100.29,100.29,0,0,0,6.69-11.44,5.12,5.12,0,0,0,.53-2.11c0-1.35-.65-2-1.94-2a4.07,4.07,0,0,0-1.49.27,30.31,30.31,0,0,1-6.78,5.1q-4.66,2.73-7.83,2.73c-2,0-3-.82-3-2.46a11.41,11.41,0,0,1,.88-3.61,11.77,11.77,0,0,0,.88-3.61.94.94,0,0,0-1.06-1.06c-1.88,0-3.78,2.06-5.72,6.16q-2.64,5.55-2.64,8.89,0,7.49,6.78,7.48a61.08,61.08,0,0,0,8.88-1.49q-8.18,16.89-8.18,19.18a9.54,9.54,0,0,0,.18,2.38c.23.7.85,1,1.84,1s2.2-1.07,4-3.21,2.64-3.68,2.64-4.62a3.6,3.6,0,0,0-.92-1.72,3.69,3.69,0,0,1-.92-2.24C1487.49,508.92,1487.75,508.22,1487.93,507.75Z" transform="translate(-45 -62.4)"></path><path d="M1732.66,516.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.62,8.62,0,0,1-1.4-5.28q0-3.26,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,1732.66,516.2Zm.8-13.73q.61-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,1733.46,502.47Z" transform="translate(-45 -62.4)"></path><path d="M1848.8,482.45a2.7,2.7,0,0,0-.35-1.59q-1.68,0-5-2.28t-3.34-3.88q0-.17,4.4-8.44t4.4-10a.39.39,0,0,0-.44-.44q-1.32,0-5.81,7.13c-2.41,3.87-4.19,6.81-5.37,8.8q-2.73-4.12-2.73-12.67a5.4,5.4,0,0,0-.17-2q-1.41,0-2.91,3.69a15.35,15.35,0,0,0-1.32,5q0,4.49,3.52,11.53-4.67,8.7-4.66,11c0,1.34.38,2,1.14,2q.88,0,2.82-3.7c1.41-2.7,2.35-4.16,2.82-4.4q2.81,2.64,4.13,4c1.7,1.59,3.32,2.38,4.84,2.38Q1848.8,488.52,1848.8,482.45Z" transform="translate(-45 -62.4)"></path><polygon points="1787 423.1 1784 423.1 1784 506.1 1790 506.1 1790 423.1 1784 423.1 1787 423.1 1784 423.1 1784 506.1 1790 506.1 1790 423.1 1784 423.1 1787 423.1"></polygon><path d="M1951.26,459.13q7.31-5.37,7.31-11,0-8.19-10.65-8.19-5.62,0-12.32,5.9t-6.69,11.44a5.74,5.74,0,0,0,3.26,5.37,13.15,13.15,0,0,0,6.42,1.41Q1944.49,464.06,1951.26,459.13Z" transform="translate(-45 -62.4)"></path><polyline points="1784.5 477.6 1884.5 466.6 1884.5 478.6 1784.5 489.6"></polyline><polyline points="1784.5 495.1 1884.5 484.1 1884.5 496.1 1784.5 507.1"></polyline><polygon points="1887 392.85 1884 392.85 1884 495.1 1890 495.1 1890 392.85 1884 392.85 1887 392.85 1884 392.85 1884 495.1 1890 495.1 1890 392.85 1884 392.85 1887 392.85"></polygon><path d="M2043.66,516.2a13.58,13.58,0,0,0-1.14,4,3.75,3.75,0,0,0,.92,2.24,3.56,3.56,0,0,1,.93,1.72q0,1.39-2.64,4.62t-4,3.21c-1.06,0-1.59-.59-1.59-1.76q0-4.32,3.79-14.87a23.41,23.41,0,0,1-5.46.79q-6.24,0-6.25-7.57,0-5.1,4-11.44a8.62,8.62,0,0,1-1.4-5.28q0-3.26,2.64-8.89c1.93-4.16,3.84-6.25,5.72-6.25a.94.94,0,0,1,1,1.06,11.47,11.47,0,0,1-.88,3.61,11.41,11.41,0,0,0-.88,3.61c0,1.7,1,2.55,3,2.55,2.23,0,4.79-.94,7.66-2.82a72.22,72.22,0,0,0,6.51-5.1,2.46,2.46,0,0,1,1.41-.35c1.29,0,1.94.7,1.94,2.11a4.38,4.38,0,0,1-.44,2.11q-6.35,11.88-8.28,16A107.34,107.34,0,0,0,2043.66,516.2Zm.8-13.73q.61-1.59,2-4.75a71.63,71.63,0,0,1-8.62,1.58,11.86,11.86,0,0,1-1.5-.09,8.67,8.67,0,0,0-.53,2.55c0,1.65,1,2.47,3.08,2.47A12.69,12.69,0,0,0,2044.46,502.47Z" transform="translate(-45 -62.4)"></path></g><g class="annotation annotations1"><circle class="annotation annotations1" cx="381.19" cy="272.07" r="43.58" style="fill:none;stroke:#ff5bae;stroke-miterlimit:10;stroke-width:8px"></circle><circle class="annotation annotations1" cx="951.65" cy="388.01" r="45.76" style="fill:none;stroke:#ff5bae;stroke-miterlimit:10;stroke-width:8px"></circle><circle class="annotation annotations1" cx="604.03" cy="380.54" r="54.17" style="fill:none;stroke:#ff5bae;stroke-miterlimit:10;stroke-width:8px"></circle><g><path class="annotation annotations1" d="M702.69,88.52c0,1,0,2,0,2.87a48.23,48.23,0,0,1-.36,7,2.75,2.75,0,0,1-1.83,1,39.08,39.08,0,0,1-.4-6.8q-.07-5.44-.36-5.8-1.87,5.55-7.16,5.54-7.85,0-8.14-13.68a27.76,27.76,0,0,1,.51-4.82,24.05,24.05,0,0,1,2.37-7.52c1.42-2.6,3.21-3.89,5.37-3.89A4.68,4.68,0,0,1,697,64.52a17.18,17.18,0,0,1,1.44,4.83,23.1,23.1,0,0,1,.54,4.46c0,1-.16,1.52-.47,1.55A2.59,2.59,0,0,1,695.92,74l-1.08-2.8a2.14,2.14,0,0,0-2-1.55c-1.48,0-2.79,1.47-3.92,4.43a17,17,0,0,0-1.3,5.79,5.68,5.68,0,0,0,1.08,3.46,4.41,4.41,0,0,0,3.78,1.65,5.67,5.67,0,0,0,2.45-.46,3.3,3.3,0,0,0,1.15-.8,2.51,2.51,0,0,0,.33-.46h-5.15l-.11-5.84q1.44-1.19,6-1.18,3.81,0,5,5.25A41.45,41.45,0,0,1,702.69,88.52Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M718.17,83.28l-1.15.18a45,45,0,0,1,.82,7.34c0,.36,0,.64,0,.83a2.12,2.12,0,0,1-1.73.61,1.53,1.53,0,0,1-.94-.21A13.28,13.28,0,0,1,714,87.81c-.17-1.31-.35-2.62-.54-3.92a6.4,6.4,0,0,1-.94.36,9.91,9.91,0,0,1-2.23.32,59.9,59.9,0,0,1,0,7.13,3.45,3.45,0,0,1-1.58.33,2.75,2.75,0,0,1-1.41-.29,7.72,7.72,0,0,1-.25-2.77V85.91A5.64,5.64,0,0,0,707,85l-1.83.43a14.69,14.69,0,0,1-.65-4.17,5.13,5.13,0,0,1,.21-1.62l1.52-.22c-.08-1.68-.11-4.77-.11-9.29a2.32,2.32,0,0,1,1.91-.75,3.11,3.11,0,0,1,1.33.28c.09.77.17,3.31.21,7.6a12.51,12.51,0,0,0,.15,1.8l3.09-.61V68.16a2.84,2.84,0,0,1,1.26-.29,2.38,2.38,0,0,1,1.84.61,48,48,0,0,1,.31,5c.08,2.73.15,4.27.19,4.63,0,0,0,0,.08,0a2.15,2.15,0,0,0,.79-.47C717.92,78.63,718.22,80.5,718.17,83.28Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M731.16,80.29a23.73,23.73,0,0,1-.68,7.34q-1.29,4.73-4.52,4.72-3.63,0-5-3.85A23.26,23.26,0,0,1,720,82c0-.46,0-.9,0-1.34a21.71,21.71,0,0,1,1.29-8A9.24,9.24,0,0,1,723.66,69c.63-.58,1.08-.87,1.37-.87q2,0,3.84,3.21A19.78,19.78,0,0,1,731.16,80.29Zm-3,1.15a17.7,17.7,0,0,0-.43-3.24c-.24-1-.51-1.58-.79-1.58l-.15,0a2.15,2.15,0,0,1-1.91,1.41,2.53,2.53,0,0,1-1.36-.47c-.44.39-.65,1.43-.65,3.13a8.74,8.74,0,0,0,.72,3.28c.6,1.42,1.32,2.12,2.16,2.12s1.6-.63,2-1.91A8.08,8.08,0,0,0,728.14,81.44Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M747.8,84.32q-.22,4.55-1.8,6.19c-1,1-2.7,1.52-5.15,1.52H735.7c0-.24.08-1.32.25-3.23s.25-2.89.25-3a6.23,6.23,0,0,1,3.17-1.33c.27,0,.51.25.72.74a1.38,1.38,0,0,0,1.41.74h1.29q.76,0,.9-.36c.07-.46-.43-.87-1.51-1.22a13.75,13.75,0,0,0-4.18-.55c-.57,0-1.14,0-1.69.08a4.69,4.69,0,0,1-2.55-1.08c-.63-.75-.94-2.25-.94-4.5a8.93,8.93,0,0,1,2-5.76,12.3,12.3,0,0,1,4-3.14,6.93,6.93,0,0,1,2.67-1c.31,0,.72.33,1.22,1a3,3,0,0,1,.76,1.77,30.85,30.85,0,0,1-.47,3.06,7.56,7.56,0,0,0-4.18,1.15c-1.13.67-1.73,1.4-1.8,2.2-.07.29.34.43,1.23.43l1.09,0,1.32,0a9.18,9.18,0,0,1,4.86,1Q748,80.54,747.8,84.32Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M761.62,67.58a43.5,43.5,0,0,1,.25,4.39c0,1.76-.12,2.63-.36,2.63q-1.15-.13-4.68,1.8l.5,14.51a2.83,2.83,0,0,1-1.51.86,9.53,9.53,0,0,0-1.58.4l-.79-14.36a22.71,22.71,0,0,1-4.18,1.94.14.14,0,0,1-.11,0l-.21-6.8q2.19-1.22,4-2.16a48.34,48.34,0,0,1,5.86-2.81A9.84,9.84,0,0,1,761.62,67.58Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M788.11,88.68a15.47,15.47,0,0,1-.36,3.49h-2.12q-.9-1.29-4.25-5a61,61,0,0,0-5.61-5.76c0,1.68.12,3.43.28,5.26s.26,2.75.26,3.06a3.38,3.38,0,0,1-.11.93c-.17.39-.57.58-1.19.58a3,3,0,0,1-1.4-.36q-1.05-3.34-1.05-13.75a61,61,0,0,1,.47-6.88,3.58,3.58,0,0,1,1.15-.22,2.62,2.62,0,0,1,1.51.47,2.75,2.75,0,0,1,.26,1.15,11.18,11.18,0,0,0,.07,1.27q2.78,2.84,6.69,7.37-.39-4.2-.39-6.08c0-.5.07-1.42.21-2.74a19.88,19.88,0,0,1,.65-3.63,5.52,5.52,0,0,0,.76-.07,5.66,5.66,0,0,1,.72-.08,1.37,1.37,0,0,1,1.26.8c0,.26,0,.94-.11,2s-.11,2.1-.11,3c0,1.39.07,2.86.22,4.43a31.73,31.73,0,0,0,1.37,6.65A15.11,15.11,0,0,1,788.11,88.68Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M801.25,80.29a23.73,23.73,0,0,1-.68,7.34q-1.29,4.73-4.52,4.72c-2.41,0-4.09-1.28-5-3.85a23.26,23.26,0,0,1-1-6.55c0-.46,0-.9,0-1.34a21.71,21.71,0,0,1,1.29-8A9.24,9.24,0,0,1,793.75,69c.63-.58,1.08-.87,1.37-.87q2,0,3.84,3.21A19.78,19.78,0,0,1,801.25,80.29Zm-3,1.15a17.7,17.7,0,0,0-.43-3.24c-.24-1-.51-1.58-.79-1.58a.4.4,0,0,0-.15,0A2.15,2.15,0,0,1,795,78.06a2.53,2.53,0,0,1-1.36-.47c-.44.39-.65,1.43-.65,3.13a8.74,8.74,0,0,0,.72,3.28c.6,1.42,1.32,2.12,2.16,2.12s1.61-.63,2-1.91A8.08,8.08,0,0,0,798.23,81.44Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M815.37,67.58c.16,1.66.25,3.12.25,4.39,0,1.76-.12,2.63-.36,2.63q-1.15-.13-4.68,1.8l.5,14.51a2.83,2.83,0,0,1-1.51.86,9.25,9.25,0,0,0-1.58.4l-.8-14.36A22.8,22.8,0,0,1,803,79.75a.12.12,0,0,1-.11,0l-.22-6.8,4-2.16a48.23,48.23,0,0,1,5.87-2.81A9.84,9.84,0,0,1,815.37,67.58Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M828.76,86.48q.18.15.18.9a14.77,14.77,0,0,1-.15,1.8c0,.39-.09.78-.14,1.19a1.51,1.51,0,0,1-1.15,1.55,6.75,6.75,0,0,1-1.62.11h-7.2q-.9-1.8-1.66-14.8-.13-2.78,3.64-5.94,3.32-2.78,5.61-3.1c.89-.11,1.34.56,1.34,2l-.08,1a6.1,6.1,0,0,1-.46,2.41,2.35,2.35,0,0,1-1.19.86,12.64,12.64,0,0,0-2.38,1.3Q820,78.28,820,79.17c0,.17.12.26.36.26q5.32-2.21,5.54-2.2c.56,0,.92.43,1.08,1.3a12.29,12.29,0,0,1,.08,1.87c0,1.61-.4,2.66-1.26,3.17l-4.76,1.94a3.9,3.9,0,0,1,0,.79c2.21-.09,3.8-.14,4.79-.14C827.68,86.16,828.66,86.27,828.76,86.48Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M845.46,84.32q-.21,4.55-1.8,6.19T838.51,92h-5.14c0-.24.08-1.32.25-3.23s.25-2.89.25-3A6.23,6.23,0,0,1,837,84.5c.26,0,.5.25.72.74a1.36,1.36,0,0,0,1.4.74h1.3q.75,0,.9-.36c.07-.46-.43-.87-1.51-1.22a13.84,13.84,0,0,0-4.18-.55c-.58,0-1.14,0-1.69.08a4.77,4.77,0,0,1-2.56-1.08q-.93-1.12-.93-4.5a8.93,8.93,0,0,1,2-5.76,12.3,12.3,0,0,1,4-3.14,6.9,6.9,0,0,1,2.66-1c.31,0,.72.33,1.23,1a3,3,0,0,1,.75,1.77,27.82,27.82,0,0,1-.47,3.06,7.55,7.55,0,0,0-4.17,1.15c-1.13.67-1.73,1.4-1.8,2.2-.07.29.33.43,1.22.43l1.1,0,1.31,0a9.14,9.14,0,0,1,4.86,1C844.83,80.05,845.58,81.8,845.46,84.32Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path></g><g><path class="annotation annotations1" d="M417.17,676.56,416,678.61a4.43,4.43,0,0,0-.5,2.27,4.8,4.8,0,0,0,1.29,3.13l-5,3.85c-.53-.79-.86-1.3-1-1.54a5.32,5.32,0,0,1,.22-4,16.45,16.45,0,0,1,1.12-2Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M432.18,695.57c0,.33-.86.77-2.59,1.29,0,.1.13.94.4,2.52a25.28,25.28,0,0,1,.39,3.93,9,9,0,0,1-.14,1.65H428.4c0-.16-.23-1.39-.54-3.67a13.32,13.32,0,0,0-1-3.74,20.18,20.18,0,0,1-6.66,1.58,3,3,0,0,1-2.09-.56,2.81,2.81,0,0,1-.57-2q0-2.73,2.05-8a62,62,0,0,1,4.28-8.93c1.66-2.8,2.87-4.28,3.64-4.42a4,4,0,0,1,.47,1.94c0,.17,0,.7-.06,1.58s-.05,1.74-.05,2.56q0,4.07.5,9.94c.67-.17,1.79-.41,3.35-.72C432,690.94,432.18,692.64,432.18,695.57ZM426.39,692a55.32,55.32,0,0,1-.8-8.67,36.25,36.25,0,0,0-2.55,4.53,33.45,33.45,0,0,0-2.2,5.15A18.45,18.45,0,0,0,426.39,692Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M446.73,692.22a6.21,6.21,0,0,1-.62,1.87c-.43,1-.75,1.51-1,1.59a27.22,27.22,0,0,1-4.07.46c-2,.12-3.44.18-4.42.18s-1.48,0-1.7,0c-.74-.15-1.11-.47-1.11-1a4.65,4.65,0,0,1,.75-1.93c.51-.88.94-1.31,1.3-1.31l.95,0c.42,0,.74,0,1,0q1.36,0,4.05-.27c1.79-.18,3.1-.27,3.94-.27C446.41,691.61,446.73,691.81,446.73,692.22Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M460.8,694.13a13.67,13.67,0,0,1-2.36,7.4q-2.35,3.69-4.59,3.69-2.81,0-2.8-3.78a4.57,4.57,0,0,1,2-3.71,1.74,1.74,0,0,1,1,1.55,2.23,2.23,0,0,1-.07.5c1-.26,2-1.19,3.09-2.77a6.81,6.81,0,0,0,1.37-2.88q0-.4-.57-.72a4.23,4.23,0,0,0-2.09-.43,4.94,4.94,0,0,0-2.81.93,18.33,18.33,0,0,0-2.16,1.84c-.82,0-1.23-.7-1.23-2.09,0-.24,0-.68,0-1.33s0-1.18,0-1.59q5.22-7.66,5.22-9.43c0-.43-.21-.61-.61-.54a3.1,3.1,0,0,0-2.24,1.48,9.29,9.29,0,0,0-1.18,2.7c-.22.89-.44,1.77-.65,2.66a1.16,1.16,0,0,1-.83.4,1,1,0,0,1-.76-.4,8.81,8.81,0,0,1-.21-1.94,16.61,16.61,0,0,1,.47-3.6,11,11,0,0,1,2-4.68,4.77,4.77,0,0,1,3.89-2,2.25,2.25,0,0,1,2,1.26,6.09,6.09,0,0,1,.61,2.88,13.86,13.86,0,0,1-.72,4.68,29.83,29.83,0,0,1-1.91,4,8.34,8.34,0,0,1,1.62-.18Q460.8,688,460.8,694.13Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M469.19,681a3.18,3.18,0,0,1-.29,1.29c-.21.39-.42.77-.61,1.16l-5,3.85a9,9,0,0,0,.9-2.77,4.06,4.06,0,0,0-.47-2.31,3.08,3.08,0,0,0-2.19-.72l5-3.85a2.38,2.38,0,0,1,2.19,1A4.82,4.82,0,0,1,469.19,681Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M498.31,694.27a9.85,9.85,0,0,1-4,8.46,15.35,15.35,0,0,1-9.25,2.56,4.59,4.59,0,0,1-3.4-1,5,5,0,0,1-1-3.49,4.23,4.23,0,0,1,1.91-3.78q0-1-.21-4.11-.07-1.26-.09-2.28c0-.69,0-1.29,0-1.82a7.06,7.06,0,0,1-1.69.29c-1,0-1.44-.88-1.44-2.63,0-2.06,1.18-4.42,3.53-7.06s4.62-4,6.66-4q3.6,0,3.6,4a13.62,13.62,0,0,1-1.33,6.12,7.56,7.56,0,0,1-.42.79c-.15.26-.31.53-.48.79a7.61,7.61,0,0,1,5.36,1.95A6.84,6.84,0,0,1,498.31,694.27Zm-8-13.1c0-.43-.33-.65-1-.65a3.75,3.75,0,0,0-2.74,1.4,4.16,4.16,0,0,0-1.3,2.85s0,.26,0,.7,0,1.09.06,1.93a17.84,17.84,0,0,0,3.09-2.74C489.66,683.27,490.28,682.1,490.28,681.17ZM495,694.38q0-2.45-3.74-2.45a7.51,7.51,0,0,0-3.35,1c-1.47.75-2.2,1.56-2.2,2.45V698a4,4,0,0,0,.25,1.58,19.42,19.42,0,0,0,3.08-.29,13.38,13.38,0,0,0,2.65-.75Q495,697.15,495,694.38Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M514.44,704.68c-2.26.21-4.07.32-5.44.32q-2.37,0-6.11-.32c-.33-1-.65-2-1-2.92a53.74,53.74,0,0,1-1.1-6.77,91.58,91.58,0,0,1-.39-9.61,16.56,16.56,0,0,1,.25-3.74,3.3,3.3,0,0,1,2.34.61,45.33,45.33,0,0,1,.43,5.83c.14,3.46.4,6.73.76,9.83,1.82.19,3.18.29,4.06.29l5.33-.36Q513.61,697.95,514.44,704.68Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M530.28,695.5q0,10.18-4.48,9.9a22.53,22.53,0,0,1-6-1,24.67,24.67,0,0,1-2.67-7.77,52.83,52.83,0,0,1-.46-7.85c0-.65,0-1.74.14-3.28s.1-2.44.07-3.16h3.14c0,.52-.06,1.53-.17,3s-.16,2.63-.16,3.42q0,5.66,1,7.78a3.91,3.91,0,0,0,3.6,2.3c1.4.1,2.3-.25,2.71-1a8.34,8.34,0,0,0,.47-3.53q0-5.58-3.6-6.34l.58-5.79c.24,0,.71,0,1.4,0,.94,0,1.85,1,2.74,3.1A25.8,25.8,0,0,1,530.28,695.5Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M543.74,699.6q.18.15.18.9a14.52,14.52,0,0,1-.14,1.8c0,.38-.1.78-.14,1.19a1.53,1.53,0,0,1-1.16,1.55,7,7,0,0,1-1.62.1h-7.2q-.9-1.8-1.65-14.79-.15-2.77,3.63-5.94,3.32-2.77,5.62-3.1c.89-.12,1.33.55,1.33,2l-.07,1a6.06,6.06,0,0,1-.47,2.41,2.31,2.31,0,0,1-1.19.86,12.54,12.54,0,0,0-2.37,1.3q-3.5,2.48-3.49,3.38c0,.17.12.25.36.25q5.32-2.19,5.54-2.19.83,0,1.08,1.29a13.12,13.12,0,0,1,.07,1.88c0,1.6-.39,2.66-1.26,3.16l-4.75,2a2.77,2.77,0,0,1,0,.79c2.21-.1,3.81-.14,4.79-.14C542.66,699.28,543.65,699.38,543.74,699.6Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M560.45,697.44c-.15,3-.75,5.09-1.8,6.19s-2.7,1.51-5.15,1.51h-5.15c0-.24.09-1.31.25-3.22s.26-2.9.26-3a6.11,6.11,0,0,1,3.16-1.33c.27,0,.51.25.72.74a1.38,1.38,0,0,0,1.41.74h1.29q.76,0,.9-.36c.08-.46-.43-.87-1.51-1.23a14.07,14.07,0,0,0-4.17-.54c-.58,0-1.14,0-1.7.07a4.54,4.54,0,0,1-2.55-1.08c-.63-.74-.94-2.24-.94-4.5a9,9,0,0,1,2-5.76,12.28,12.28,0,0,1,4-3.13,6.81,6.81,0,0,1,2.67-1c.31,0,.72.32,1.22,1a3,3,0,0,1,.76,1.77,30.85,30.85,0,0,1-.47,3.06,7.58,7.58,0,0,0-4.18,1.15,3.15,3.15,0,0,0-1.8,2.19c-.07.29.34.44,1.23.44l1.1,0,1.31,0a9.24,9.24,0,0,1,4.86,1Q560.63,693.67,560.45,697.44Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M576.18,682.18a13.94,13.94,0,0,1,.22,2.37,13,13,0,0,1-4.61,9.87c.14,3.69.21,5.53.21,5.5a39.43,39.43,0,0,1-.32,5.12h-3.13a84,84,0,0,0-.4-8.64,9.24,9.24,0,0,1-2,.25q-4.57,0-4.57-6.84c0-1.11.1-2.59.29-4.47a2.83,2.83,0,0,1,2.55-1.22,4,4,0,0,1,1.41.25,20.32,20.32,0,0,0-.65,4c0,1.27.42,1.91,1.26,1.91a4.26,4.26,0,0,0,.47,0,7.64,7.64,0,0,0,4.28-2.92,7.5,7.5,0,0,0,1.95-4.68,4,4,0,0,0-.11-.93,4,4,0,0,1,1.8-.47C575.74,681.24,576.18,681.55,576.18,682.18Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M600.44,701.8a5.28,5.28,0,0,1-1,3.2h-9.29q-2.7-1.65-3.13-9.76a21.79,21.79,0,0,1,1.23-8.1c1.22-3.67,2.94-5.5,5.14-5.5q4.32,0,4.72,7.34a20.69,20.69,0,0,1-.4,6.26,2.35,2.35,0,0,0-1.44.44c-1.15,0-1.72-1.07-1.72-3.21,0-.31,0-.77,0-1.37s0-1,0-1.29c0-1.39-.3-2.09-.9-2.09s-1.39.7-2.31,2.09a9,9,0,0,0-1.47,5.43,6.92,6.92,0,0,0,.57,2.74,2.46,2.46,0,0,0,1.91,1.44h7.06c.24.34.47.67.68,1A3,3,0,0,1,600.44,701.8Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M615.38,705.4a48.3,48.3,0,0,0-6.48-.51c-1.08,0-2.19.05-3.34.15a54.28,54.28,0,0,1-1.34-10.91c-1.36-.27-2.05-1.73-2.05-4.39a8.58,8.58,0,0,1,2.41-5.87q2.31-2.49,4.65-2.49,3.09,0,4.17,2.24a10.32,10.32,0,0,1,.69,4.35,12,12,0,0,1-1.33,5.37A31.78,31.78,0,0,1,609,699c0,.45,1,.75,3.08.88s3.14.27,3.19.41ZM610.92,688c0-.55-.46-.83-1.37-.83a1,1,0,0,0-.39.08,6.11,6.11,0,0,0-3.75,3.31,3.74,3.74,0,0,1,.9,2.34c.07.65.13,1.29.18,1.94q.25-.43,2.77-3.6A6,6,0,0,0,610.92,688Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M630.86,695.5q0,10.18-4.48,9.9a22.53,22.53,0,0,1-6-1,24.67,24.67,0,0,1-2.67-7.77,54.24,54.24,0,0,1-.46-7.85c0-.65.05-1.74.15-3.28s.09-2.44.07-3.16h3.13c0,.52,0,1.53-.16,3s-.16,2.63-.16,3.42q0,5.66.95,7.78a3.91,3.91,0,0,0,3.6,2.3c1.4.1,2.31-.25,2.72-1a8.51,8.51,0,0,0,.47-3.53q0-5.58-3.6-6.34l.57-5.79c.24,0,.71,0,1.41,0,.93,0,1.84,1,2.73,3.1A25.8,25.8,0,0,1,630.86,695.5Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M648.43,701.8a15.47,15.47,0,0,1-.36,3.49H646q-.9-1.31-4.25-5a57.25,57.25,0,0,0-5.62-5.76c0,1.68.12,3.43.29,5.25s.25,2.75.25,3.06a3.46,3.46,0,0,1-.1.94,1.17,1.17,0,0,1-1.19.58,3,3,0,0,1-1.41-.36q-1-3.36-1-13.76a60.71,60.71,0,0,1,.47-6.87,3.33,3.33,0,0,1,1.15-.22,2.62,2.62,0,0,1,1.51.47,2.58,2.58,0,0,1,.25,1.15,12.22,12.22,0,0,0,.08,1.26c1.84,1.9,4.08,4.36,6.69,7.38-.26-2.81-.39-4.83-.39-6.08,0-.51.07-1.42.21-2.74a18.71,18.71,0,0,1,.65-3.63,5.92,5.92,0,0,0,.76-.08c.33,0,.57-.07.72-.07a1.38,1.38,0,0,1,1.26.79c0,.27,0,1-.11,2s-.11,2.09-.11,3c0,1.4.07,2.87.22,4.43a32.62,32.62,0,0,0,1.36,6.66A14.83,14.83,0,0,1,648.43,701.8Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M663.66,701.8a5.34,5.34,0,0,1-1,3.2H653.4q-2.7-1.65-3.13-9.76a21.57,21.57,0,0,1,1.22-8.1c1.23-3.67,2.94-5.5,5.15-5.5q4.32,0,4.71,7.34a21,21,0,0,1-.39,6.26,2.31,2.31,0,0,0-1.44.44c-1.15,0-1.73-1.07-1.73-3.21,0-.31,0-.77,0-1.37s0-1,0-1.29c0-1.39-.3-2.09-.9-2.09s-1.39.7-2.3,2.09a9,9,0,0,0-1.48,5.43,7.07,7.07,0,0,0,.58,2.74,2.45,2.45,0,0,0,1.91,1.44h7.05c.24.34.47.67.69,1A3.32,3.32,0,0,1,663.66,701.8Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M678.85,696.4l-1.15.18a45.09,45.09,0,0,1,.83,7.34,5.9,5.9,0,0,1,0,.83,2.12,2.12,0,0,1-1.73.61,1.44,1.44,0,0,1-.93-.22,13,13,0,0,1-1.12-4.21c-.17-1.32-.35-2.63-.54-3.92-.5.21-.82.33-.93.36a10.62,10.62,0,0,1-2.24.32,55.91,55.91,0,0,1,0,7.13,3.45,3.45,0,0,1-1.58.32,2.72,2.72,0,0,1-1.4-.28,7.68,7.68,0,0,1-.26-2.78V699a7.1,7.1,0,0,0-.1-.93l-1.84.43a15.12,15.12,0,0,1-.65-4.18,4.83,4.83,0,0,1,.22-1.62l1.51-.21q-.1-2.52-.11-9.29a2.34,2.34,0,0,1,1.91-.76,3.12,3.12,0,0,1,1.33.29q.15,1.15.22,7.6a11.52,11.52,0,0,0,.14,1.8l3.1-.61V681.28a2.66,2.66,0,0,1,1.26-.29,2.33,2.33,0,0,1,1.83.61,45,45,0,0,1,.31,5c.08,2.73.15,4.27.2,4.63,0,0,0,0,.07,0a2.06,2.06,0,0,0,.79-.46Q678.93,692.21,678.85,696.4Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M696.13,677.93a99,99,0,0,0-7.2,12.38q-2.41,5.09-4.82,10.15-2.1,4.32-3.06,4.83c-.75,0-1.12-.43-1.12-1.3a8,8,0,0,1,.25-1.76q.59-2.78,6.37-14,6.16-11.94,7.89-12c1.15,0,1.73.43,1.73,1.3A2.2,2.2,0,0,1,696.13,677.93Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M710.75,693.05c0,4-.38,7-1.12,8.89a4.54,4.54,0,0,1-2.81,3,15.18,15.18,0,0,1-4,.33c-2.79,0-4.55-.61-5.3-1.84a57.35,57.35,0,0,1-.54-6.51l1.16.28a22.3,22.3,0,0,0-.83-4.86,3.39,3.39,0,0,1-1.05.33v-6.16a16.07,16.07,0,0,1,3.42-4,6.62,6.62,0,0,1,4-1.16,5.93,5.93,0,0,1,6,3.89A21.49,21.49,0,0,1,710.75,693.05Zm-2.52-.22a5.09,5.09,0,0,0-1.73-3.67,4.1,4.1,0,0,0-2.59-1.4,3.69,3.69,0,0,0-2.38.86,3.26,3.26,0,0,0-1.37,2.77,25.58,25.58,0,0,0,1,6.16,6.11,6.11,0,0,0,3,.83c1.73,0,2.84-.33,3.32-1S708.23,695.23,708.23,692.83Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M723.85,693.41a24.08,24.08,0,0,1-.68,7.34q-1.29,4.72-4.52,4.72c-2.42,0-4.09-1.29-5-3.85a23.72,23.72,0,0,1-1-6.56c0-.45,0-.9,0-1.33a21.67,21.67,0,0,1,1.29-8,9.14,9.14,0,0,1,2.44-3.64c.62-.57,1.08-.86,1.36-.86q2,0,3.84,3.2A19.82,19.82,0,0,1,723.85,693.41Zm-3,1.15a17.7,17.7,0,0,0-.44-3.24c-.24-1.06-.5-1.58-.79-1.58a.35.35,0,0,0-.14,0,2.16,2.16,0,0,1-1.91,1.41,2.56,2.56,0,0,1-1.37-.47c-.43.38-.65,1.43-.65,3.13a8.74,8.74,0,0,0,.72,3.28c.6,1.41,1.32,2.12,2.16,2.12s1.61-.64,2-1.91A8.12,8.12,0,0,0,720.83,694.56Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M739.15,695.5q0,10.18-4.48,9.9a22.53,22.53,0,0,1-6-1,24.42,24.42,0,0,1-2.67-7.77,52.83,52.83,0,0,1-.46-7.85c0-.65,0-1.74.14-3.28s.1-2.44.07-3.16h3.14c0,.52-.06,1.53-.17,3s-.16,2.63-.16,3.42q0,5.66,1,7.78a3.91,3.91,0,0,0,3.6,2.3c1.4.1,2.3-.25,2.71-1a8.34,8.34,0,0,0,.47-3.53q0-5.58-3.6-6.34l.58-5.79c.24,0,.71,0,1.4,0,.94,0,1.85,1,2.74,3.1A25.8,25.8,0,0,1,739.15,695.5Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M756.39,696.36a8.06,8.06,0,0,1-3.2,6.91,12.41,12.41,0,0,1-7.56,2.13,3.72,3.72,0,0,1-2.84-.87,4.26,4.26,0,0,1-.8-2.91,3.5,3.5,0,0,1,1.55-3.14c0-.26,0-.66,0-1.18s-.06-1.18-.11-1.95a22.28,22.28,0,0,1-.11-3.2,4.45,4.45,0,0,1-1.19.21c-.86,0-1.29-.75-1.29-2.26,0-1.76,1-3.7,2.88-5.84s3.75-3.24,5.43-3.24q3,0,3,3.35a9.32,9.32,0,0,1-1.08,5,5.33,5.33,0,0,0-.33.45,4.9,4.9,0,0,0-.36.66l.15,0a6,6,0,0,1,5.86,5.83Zm-6.73-10.55c0-.24-.21-.36-.65-.36a2.91,2.91,0,0,0-2.12,1.08,2.84,2.84,0,0,0-1,2.16l0,1.87a9.66,9.66,0,0,0,2.31-2.12A4.65,4.65,0,0,0,749.66,685.81Zm3.78,10.62a1.39,1.39,0,0,0-.41-1.06,2.68,2.68,0,0,0-1-.56,4.41,4.41,0,0,0-1.1-.2,2.71,2.71,0,0,0-.79.06,6.42,6.42,0,0,0-3,1.22c-.57.48-.86.91-.86,1.3v1.73a5.36,5.36,0,0,0,.14,1.58,16.44,16.44,0,0,0,2.43-.23,9.47,9.47,0,0,0,2.07-.6Q753.44,698.56,753.44,696.43Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M772.2,704.68c-2.26.21-4.07.32-5.44.32q-2.37,0-6.11-.32c-.33-1-.65-2-1-2.92a53.26,53.26,0,0,1-1.11-6.77,91.58,91.58,0,0,1-.39-9.61,17.21,17.21,0,0,1,.25-3.74,3.33,3.33,0,0,1,2.35.61,47.31,47.31,0,0,1,.43,5.83c.14,3.46.39,6.73.75,9.83,1.83.19,3.18.29,4.07.29l5.33-.36C771.37,697.91,771.64,700.19,772.2,704.68Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M785.45,699.6c.11.1.18.4.18.9a14.92,14.92,0,0,1-.15,1.8c-.05.38-.09.78-.14,1.19a1.52,1.52,0,0,1-1.15,1.55,7.12,7.12,0,0,1-1.62.1h-7.2q-.9-1.8-1.66-14.79c-.1-1.85,1.12-3.83,3.64-5.94,2.2-1.85,4.08-2.88,5.61-3.1.89-.12,1.33.55,1.33,2l-.07,1a6.06,6.06,0,0,1-.47,2.41,2.32,2.32,0,0,1-1.18.86,12.34,12.34,0,0,0-2.38,1.3q-3.5,2.48-3.49,3.38c0,.17.12.25.36.25q5.33-2.19,5.54-2.19.82,0,1.08,1.29a13.12,13.12,0,0,1,.07,1.88c0,1.6-.39,2.66-1.26,3.16l-4.75,2a3.9,3.9,0,0,1,0,.79c2.2-.1,3.8-.14,4.78-.14C784.37,699.28,785.35,699.38,785.45,699.6Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M800.35,692.22a6.09,6.09,0,0,1-.61,1.87c-.43,1-.76,1.51-1,1.59a27.63,27.63,0,0,1-4.07.46c-2,.12-3.45.18-4.43.18s-1.48,0-1.69,0c-.75-.15-1.12-.47-1.12-1a4.56,4.56,0,0,1,.76-1.93c.5-.88.93-1.31,1.29-1.31l1,0c.42,0,.74,0,.95,0q1.37,0,4.05-.27c1.79-.18,3.1-.27,3.94-.27C800,691.61,800.35,691.81,800.35,692.22Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M817.49,697.44c-.15,3-.75,5.09-1.8,6.19s-2.7,1.51-5.15,1.51h-5.15c0-.24.08-1.31.25-3.22s.25-2.9.25-3a6.15,6.15,0,0,1,3.17-1.33c.27,0,.51.25.72.74a1.38,1.38,0,0,0,1.41.74h1.29q.76,0,.9-.36c.07-.46-.43-.87-1.51-1.23a14.1,14.1,0,0,0-4.18-.54c-.57,0-1.14,0-1.69.07a4.54,4.54,0,0,1-2.55-1.08c-.63-.74-.94-2.24-.94-4.5a9,9,0,0,1,2-5.76,12.28,12.28,0,0,1,4-3.13,6.76,6.76,0,0,1,2.67-1c.31,0,.72.32,1.22,1a3,3,0,0,1,.76,1.77,30.85,30.85,0,0,1-.47,3.06,7.56,7.56,0,0,0-4.18,1.15q-1.69,1-1.8,2.19c-.07.29.34.44,1.23.44l1.09,0,1.32,0a9.29,9.29,0,0,1,4.86,1C816.85,693.17,817.6,694.92,817.49,697.44Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M831.31,680.7a43.5,43.5,0,0,1,.25,4.39c0,1.75-.12,2.63-.36,2.63q-1.16-.15-4.68,1.8L827,704a2.83,2.83,0,0,1-1.51.86,9.53,9.53,0,0,0-1.58.4l-.79-14.37a22.39,22.39,0,0,1-4.18,2,.14.14,0,0,1-.11,0l-.21-6.8q2.19-1.23,4-2.16a46.78,46.78,0,0,1,5.86-2.81A9.18,9.18,0,0,1,831.31,680.7Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M844.34,693.41a24.08,24.08,0,0,1-.68,7.34q-1.29,4.72-4.52,4.72c-2.42,0-4.09-1.29-5-3.85a23.72,23.72,0,0,1-1-6.56c0-.45,0-.9,0-1.33a21.67,21.67,0,0,1,1.29-8,9.24,9.24,0,0,1,2.44-3.64c.62-.57,1.08-.86,1.36-.86q2,0,3.84,3.2A19.82,19.82,0,0,1,844.34,693.41Zm-3,1.15a17.7,17.7,0,0,0-.44-3.24c-.24-1.06-.5-1.58-.79-1.58a.35.35,0,0,0-.14,0,2.16,2.16,0,0,1-1.91,1.41,2.56,2.56,0,0,1-1.37-.47c-.43.38-.65,1.43-.65,3.13a8.74,8.74,0,0,0,.72,3.28c.6,1.41,1.32,2.12,2.16,2.12s1.61-.64,2-1.91A8.12,8.12,0,0,0,841.32,694.56Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M856.44,687.83a21.37,21.37,0,0,1-.9,7,5.39,5.39,0,0,1-2.2,2.85c-.86.38-1.73.78-2.59,1.19,0,.36,0,1.11.11,2.26.12,1.4.19,2.67.21,3.82L850,705h-1.47q-.51-.29-1.48-7.52-.79-5.87-1.12-9.58c0-.26,0-.52,0-.79a6.32,6.32,0,0,1,1.62-4.46,4.16,4.16,0,0,1,3.13-1.55Q856.43,681.06,856.44,687.83Zm-3,1.15a2.93,2.93,0,0,0-1.08-1.37,2.22,2.22,0,0,0-1.26-.54c-.6,0-1.07.4-1.4,1.19a5.29,5.29,0,0,0-.36,2,20.1,20.1,0,0,0,.18,2.09,4.86,4.86,0,0,0,2.66-1.23A4.09,4.09,0,0,0,853.41,689Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path></g><path class="annotation annotations1" d="M756.94,961.31l1,30.1a6.29,6.29,0,0,1-1.67.54l-.54-16.42a4.15,4.15,0,0,1-1.89,1.78,5.5,5.5,0,0,1-2.56.83,3.31,3.31,0,0,1-1.13-.18l-.36-26.64,1.44-.32.41,12.69Q752.71,962.57,756.94,961.31Zm-1.49,6.07a6.78,6.78,0,0,0-3.64,2.12l.13,2.83a5.49,5.49,0,0,0,3.6-1.66C755.48,970,755.45,968.85,755.45,967.38Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M1811.22,688.33l1,30.1a6.29,6.29,0,0,1-1.67.54l-.54-16.42a4.17,4.17,0,0,1-1.88,1.78,5.56,5.56,0,0,1-2.57.83,3.31,3.31,0,0,1-1.13-.18l-.36-26.64,1.44-.32.41,12.69Q1807,689.59,1811.22,688.33Zm-1.49,6.07a6.78,6.78,0,0,0-3.64,2.12l.13,2.83a5.55,5.55,0,0,0,3.61-1.66C1809.76,697,1809.73,695.88,1809.73,694.4Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M775.9,972.73a15.25,15.25,0,0,1-2.62,8.22q-2.62,4.09-5.1,4.1-3.12,0-3.12-4.2a5,5,0,0,1,2.24-4.12,1.93,1.93,0,0,1,1.12,1.72,2.23,2.23,0,0,1-.08.56c1.12-.3,2.26-1.32,3.44-3.08a7.56,7.56,0,0,0,1.52-3.2c0-.3-.22-.56-.64-.8a4.72,4.72,0,0,0-2.32-.48,5.4,5.4,0,0,0-3.12,1,19.17,19.17,0,0,0-2.4,2c-.91,0-1.36-.78-1.36-2.32,0-.27,0-.76,0-1.48s0-1.31,0-1.76q5.79-8.52,5.8-10.48c0-.48-.23-.68-.68-.6a3.45,3.45,0,0,0-2.48,1.64,10.08,10.08,0,0,0-1.32,3l-.72,3a1.33,1.33,0,0,1-.92.44,1.15,1.15,0,0,1-.84-.44,9.66,9.66,0,0,1-.24-2.16,18.81,18.81,0,0,1,.52-4,12.22,12.22,0,0,1,2.24-5.2,5.29,5.29,0,0,1,4.32-2.24,2.47,2.47,0,0,1,2.24,1.4,6.63,6.63,0,0,1,.68,3.2,15.56,15.56,0,0,1-.8,5.2,33.48,33.48,0,0,1-2.12,4.4,8.68,8.68,0,0,1,1.8-.2Q775.9,965.89,775.9,972.73Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M682.88,914.08Q681.71,923,677.32,942a.64.64,0,0,1-.36.62l-.76.38a9.75,9.75,0,0,1-1-.72,2.17,2.17,0,0,1-.44-1.52q0-2.25,1.84-10.52a109,109,0,0,1,2.72-10.76,1.23,1.23,0,0,0,.16-.56.85.85,0,0,0-.22-.48A1.27,1.27,0,0,1,679,918c-1-.08-2-.12-2.84-.12a27.69,27.69,0,0,0-9.32,2v-7.34a11.09,11.09,0,0,1,4.68-1.75,27.63,27.63,0,0,1,5.84-.67q5.59,0,5.6,3.43C682.92,913.67,682.9,913.87,682.88,914.08Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M683,972.73a15.25,15.25,0,0,1-2.62,8.22q-2.62,4.09-5.1,4.1-3.12,0-3.12-4.2a5,5,0,0,1,2.24-4.12,1.93,1.93,0,0,1,1.12,1.72,2.23,2.23,0,0,1-.08.56c1.12-.3,2.26-1.32,3.44-3.08a7.56,7.56,0,0,0,1.52-3.2c0-.3-.22-.56-.64-.8a4.61,4.61,0,0,0-2.32-.48,5.4,5.4,0,0,0-3.12,1,19.17,19.17,0,0,0-2.4,2c-.91,0-1.36-.78-1.36-2.32,0-.27,0-.76,0-1.48s0-1.31,0-1.76q5.79-8.52,5.8-10.48c0-.48-.23-.68-.68-.6a3.4,3.4,0,0,0-2.48,1.64,10.08,10.08,0,0,0-1.32,3l-.72,3a1.33,1.33,0,0,1-.92.44,1.13,1.13,0,0,1-.84-.44,9.66,9.66,0,0,1-.24-2.16,18.81,18.81,0,0,1,.52-4,12.32,12.32,0,0,1,2.24-5.21,5.3,5.3,0,0,1,4.32-2.23,2.47,2.47,0,0,1,2.24,1.4,6.63,6.63,0,0,1,.68,3.2,15.56,15.56,0,0,1-.8,5.2,33.54,33.54,0,0,1-2.12,4.39,9.35,9.35,0,0,1,1.8-.19Q683,965.89,683,972.73Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M664.71,972.66a37.91,37.91,0,0,1-.63,8.59,13.85,13.85,0,0,1-4.86,6.89h-1.35l-1.8-33.84c.09-.06.69-.29,1.8-.68l1,15.3a4.51,4.51,0,0,1,2.56-1.12,2.84,2.84,0,0,1,2.39,1.8A6.16,6.16,0,0,1,664.71,972.66Zm-2.25,3.15q0-1.17-.63-1.44a5.06,5.06,0,0,0-2.7,2.74l.18,4.14a6.88,6.88,0,0,0,2.11-2.45A6.29,6.29,0,0,0,662.46,975.81Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M518.57,913.57q-1.17,8.91-5.56,27.89a.64.64,0,0,1-.36.62l-.76.37a8.06,8.06,0,0,1-1-.72,2.21,2.21,0,0,1-.44-1.52q0-2.24,1.84-10.52A110.59,110.59,0,0,1,515,918.93a1.16,1.16,0,0,0,.16-.56.83.83,0,0,0-.22-.48,1.12,1.12,0,0,1-.26-.44c-1-.08-2-.12-2.84-.12a27.94,27.94,0,0,0-9.32,2V912a11.09,11.09,0,0,1,4.68-1.75,27.67,27.67,0,0,1,5.84-.68q5.6,0,5.6,3.44C518.61,913.16,518.59,913.36,518.57,913.57Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M519.57,973.81q0,.57-2.88,1.44c0,.11.14,1,.44,2.8a28,28,0,0,1,.44,4.36,9.13,9.13,0,0,1-.16,1.84h-2q-.07-.27-.6-4.08a15.08,15.08,0,0,0-1.08-4.16,22.19,22.19,0,0,1-7.4,1.76,3.31,3.31,0,0,1-2.32-.62,3.08,3.08,0,0,1-.64-2.26c0-2,.76-5,2.28-8.92a68.49,68.49,0,0,1,4.76-9.92q2.76-4.68,4-4.92a4.54,4.54,0,0,1,.52,2.16c0,.19,0,.78-.06,1.76s-.06,1.94-.06,2.84q0,4.53.56,11c.75-.18,2-.45,3.72-.8Q519.57,968.94,519.57,973.81Zm-6.44-4a61.21,61.21,0,0,1-.88-9.64,39.54,39.54,0,0,0-2.84,5A36.43,36.43,0,0,0,507,971,20.63,20.63,0,0,0,513.13,969.85Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M500.4,929.25a38,38,0,0,1-.63,8.6,13.83,13.83,0,0,1-4.86,6.88h-1.35l-1.8-33.84c.09-.06.69-.28,1.8-.67l1,15.3a4.56,4.56,0,0,1,2.57-1.13c.93,0,1.72.61,2.38,1.8A6.19,6.19,0,0,1,500.4,929.25Zm-2.25,3.15q0-1.17-.63-1.44a5.09,5.09,0,0,0-2.7,2.75l.18,4.14a7,7,0,0,0,2.12-2.45A6.32,6.32,0,0,0,498.15,932.4Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M1416.86,688.33l1,30.1a6.19,6.19,0,0,1-1.66.54l-.54-16.42a4.15,4.15,0,0,1-1.89,1.78,5.53,5.53,0,0,1-2.57.83,3.25,3.25,0,0,1-1.12-.18l-.36-26.64,1.44-.32.4,12.69C1412.27,690,1414,689.17,1416.86,688.33Zm-1.48,6.07a6.81,6.81,0,0,0-3.65,2.12l.14,2.83a5.49,5.49,0,0,0,3.6-1.66C1415.41,697,1415.38,695.88,1415.38,694.4Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M663.81,929.25a38,38,0,0,1-.63,8.6,13.83,13.83,0,0,1-4.86,6.88H657l-1.8-33.83a16.71,16.71,0,0,1,1.8-.68l1,15.3a4.51,4.51,0,0,1,2.56-1.12,2.84,2.84,0,0,1,2.39,1.79A6.19,6.19,0,0,1,663.81,929.25Zm-2.25,3.15c0-.78-.21-1.25-.63-1.44a5.12,5.12,0,0,0-2.7,2.75l.18,4.14a7,7,0,0,0,2.11-2.45A6.35,6.35,0,0,0,661.56,932.4Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M1682.85,700.15a15.2,15.2,0,0,1-2.62,8.22q-2.62,4.1-5.1,4.1-3.12,0-3.12-4.2a5.06,5.06,0,0,1,2.24-4.12,1.94,1.94,0,0,1,1.12,1.72,2.16,2.16,0,0,1-.08.56c1.12-.29,2.26-1.32,3.44-3.08a7.56,7.56,0,0,0,1.52-3.2c0-.29-.21-.56-.64-.8a4.72,4.72,0,0,0-2.32-.48,5.47,5.47,0,0,0-3.12,1,21.21,21.21,0,0,0-2.4,2c-.91,0-1.36-.77-1.36-2.32,0-.27,0-.76,0-1.48s0-1.31,0-1.76q5.79-8.52,5.8-10.48c0-.48-.23-.68-.68-.6a3.43,3.43,0,0,0-2.48,1.64,10.08,10.08,0,0,0-1.32,3l-.72,3a1.33,1.33,0,0,1-.92.44,1.15,1.15,0,0,1-.84-.44,9.53,9.53,0,0,1-.24-2.16,18.89,18.89,0,0,1,.52-4,12.27,12.27,0,0,1,2.24-5.2,5.31,5.31,0,0,1,4.32-2.24,2.48,2.48,0,0,1,2.24,1.4,6.66,6.66,0,0,1,.68,3.2,15.52,15.52,0,0,1-.8,5.2,32.87,32.87,0,0,1-2.12,4.4,9.42,9.42,0,0,1,1.8-.2Q1682.85,693.31,1682.85,700.15Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M1686.73,738.83a19.79,19.79,0,0,1-3.16,11.44q-2.8,4.13-6.12,4.12a5.13,5.13,0,0,1-4.08-2.36,11.32,11.32,0,0,1-2-7,67.14,67.14,0,0,1,1.68-12.76q2.23-10.92,5.48-10.92c1.28,0,1.92.57,1.92,1.72a9.35,9.35,0,0,1-1,4.84c-.11.16-.4.22-.88.18s-.78,0-.88.18q-1.8,3.53-2.76,11.44a19.67,19.67,0,0,0-.16,2.44,5.6,5.6,0,0,0,.68,3.24,26,26,0,0,1,2.56-9.08q2.52-5.44,5-5.44,2.35,0,3.24,3A21.37,21.37,0,0,1,1686.73,738.83Zm-2.72,1.36c0-1.33-.27-2.17-.8-2.52q-1.17,0-3.3,4.26c-1.43,2.84-2,4.57-1.82,5.18,1.52.11,3-1,4.28-3.16A9,9,0,0,0,1684,740.19Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M1436.71,725.5q-1.17,8.91-5.57,27.89a.63.63,0,0,1-.35.62l-.76.38a8.52,8.52,0,0,1-1-.72,2.22,2.22,0,0,1-.44-1.52q0-2.24,1.84-10.52a109,109,0,0,1,2.73-10.76,1.19,1.19,0,0,0,.16-.56.86.86,0,0,0-.23-.48,1.28,1.28,0,0,1-.25-.44c-1-.08-2-.12-2.84-.12a27.74,27.74,0,0,0-9.33,2V723.9a11,11,0,0,1,4.68-1.75,27.75,27.75,0,0,1,5.84-.68q5.61,0,5.61,3.44C1436.75,725.09,1436.73,725.29,1436.71,725.5Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M1435.77,698a15.2,15.2,0,0,1-2.62,8.22c-1.75,2.74-3.45,4.1-5.1,4.1q-3.12,0-3.12-4.2a5.06,5.06,0,0,1,2.24-4.12,1.94,1.94,0,0,1,1.12,1.72,2.1,2.1,0,0,1-.08.56c1.12-.29,2.26-1.32,3.44-3.08a7.51,7.51,0,0,0,1.52-3.2c0-.29-.22-.56-.64-.8a4.72,4.72,0,0,0-2.32-.48,5.47,5.47,0,0,0-3.12,1,21.21,21.21,0,0,0-2.4,2c-.91,0-1.36-.77-1.36-2.32,0-.26,0-.76,0-1.48s0-1.3,0-1.76q5.79-8.52,5.8-10.48c0-.48-.23-.68-.68-.6a3.46,3.46,0,0,0-2.48,1.64,10.08,10.08,0,0,0-1.32,3l-.72,3a1.29,1.29,0,0,1-.92.44,1.1,1.1,0,0,1-.84-.44,9.53,9.53,0,0,1-.24-2.16,19,19,0,0,1,.52-4,12.27,12.27,0,0,1,2.24-5.2,5.31,5.31,0,0,1,4.32-2.24,2.49,2.49,0,0,1,2.24,1.4,6.69,6.69,0,0,1,.68,3.2,15.48,15.48,0,0,1-.8,5.2,32.87,32.87,0,0,1-2.12,4.4,9.42,9.42,0,0,1,1.8-.2Q1435.77,691.19,1435.77,698Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M1418.54,741.19a38.08,38.08,0,0,1-.63,8.6,13.87,13.87,0,0,1-4.86,6.88h-1.35l-1.8-33.84c.09-.06.69-.29,1.8-.68l1,15.31a4.54,4.54,0,0,1,2.56-1.13,2.84,2.84,0,0,1,2.39,1.8A6.16,6.16,0,0,1,1418.54,741.19Zm-2.25,3.15q0-1.17-.63-1.44a5.05,5.05,0,0,0-2.7,2.75l.18,4.14a7,7,0,0,0,2.11-2.46A6.29,6.29,0,0,0,1416.29,744.34Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M1813.64,743.31a37.91,37.91,0,0,1-.63,8.59,13.85,13.85,0,0,1-4.86,6.89h-1.35L1805,725c.09-.06.7-.29,1.8-.68l1,15.3a4.56,4.56,0,0,1,2.57-1.12c.93,0,1.72.6,2.38,1.8A6.16,6.16,0,0,1,1813.64,743.31Zm-2.25,3.15q0-1.17-.63-1.44a5.09,5.09,0,0,0-2.7,2.74l.18,4.14a7,7,0,0,0,2.12-2.45A6.29,6.29,0,0,0,1811.39,746.46Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M1830.87,700.15a15.11,15.11,0,0,1-2.62,8.22q-2.61,4.1-5.1,4.1-3.12,0-3.12-4.2a5.09,5.09,0,0,1,2.24-4.12,1.93,1.93,0,0,1,1.12,1.72,1.81,1.81,0,0,1-.08.56q1.68-.43,3.44-3.08a7.43,7.43,0,0,0,1.52-3.2c0-.29-.21-.56-.64-.8a4.69,4.69,0,0,0-2.32-.48,5.52,5.52,0,0,0-3.12,1,23.23,23.23,0,0,0-2.4,2c-.9,0-1.36-.77-1.36-2.32,0-.27,0-.76,0-1.48s0-1.31,0-1.76q5.81-8.52,5.8-10.48c0-.48-.22-.68-.68-.6a3.45,3.45,0,0,0-2.48,1.64,10.62,10.62,0,0,0-1.32,3l-.72,3a1.31,1.31,0,0,1-.92.44,1.17,1.17,0,0,1-.84-.44,9.53,9.53,0,0,1-.24-2.16,19.57,19.57,0,0,1,.52-4,12.4,12.4,0,0,1,2.24-5.2,5.34,5.34,0,0,1,4.32-2.24,2.48,2.48,0,0,1,2.24,1.4,6.55,6.55,0,0,1,.68,3.2,15.27,15.27,0,0,1-.8,5.2,31.63,31.63,0,0,1-2.12,4.4,9.59,9.59,0,0,1,1.8-.2C1829.25,693.31,1830.87,695.59,1830.87,700.15Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M1831.81,727.62q-1.15,8.91-5.56,27.89a.64.64,0,0,1-.36.62l-.76.38a11.07,11.07,0,0,1-1-.72,2.22,2.22,0,0,1-.44-1.52q0-2.25,1.84-10.52,1.68-7.72,2.72-10.76a1.11,1.11,0,0,0,.16-.56.78.78,0,0,0-.22-.48,1.72,1.72,0,0,1-.26-.44c-1-.08-2-.12-2.84-.12a27.74,27.74,0,0,0-9.32,2V726a11.09,11.09,0,0,1,4.68-1.75,27.7,27.7,0,0,1,5.84-.67c3.74,0,5.6,1.14,5.6,3.43A4.22,4.22,0,0,1,1831.81,727.62Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><path class="annotation annotations1" d="M1665.66,698.68a37.91,37.91,0,0,1-.63,8.59,13.85,13.85,0,0,1-4.86,6.89h-1.35l-1.8-33.84c.09-.06.69-.29,1.8-.68l1,15.3a4.54,4.54,0,0,1,2.57-1.12,2.84,2.84,0,0,1,2.38,1.8A6.16,6.16,0,0,1,1665.66,698.68Zm-2.25,3.15q0-1.17-.63-1.44a5.06,5.06,0,0,0-2.7,2.74l.18,4.14a7,7,0,0,0,2.12-2.45A6.37,6.37,0,0,0,1663.41,701.83Z" transform="translate(-45 -62.4)" style="fill:#ff5bae"></path><line class="annotation annotations1" x1="663.13" y1="45.98" x2="428.49" y2="234.55" style="fill:none;stroke:#ff5bae;stroke-linejoin:round;stroke-width:4px"></line><g><line class="annotation annotations1" x1="1529.75" y1="1314.43" x2="1525.85" y2="1313.54" style="fill:none;stroke:#ff5bae;stroke-linejoin:round;stroke-width:2px"></line></g><g><line class="annotation annotations1" x1="629.96" y1="1889.18" x2="631.52" y2="1885.5" style="fill:none;stroke:#ff5bae;stroke-linejoin:round;stroke-width:2px"></line></g><line class="annotation annotations1" x1="690.31" y1="45.98" x2="622.62" y2="315.65" style="fill:none;stroke:#ff5bae;stroke-linejoin:round;stroke-width:4px"></line><line class="annotation annotations1" x1="772.22" y1="45.98" x2="926.02" y2="332.62" style="fill:none;stroke:#ff5bae;stroke-linejoin:round;stroke-width:4px"></line><line class="annotation annotations1" x1="650.33" y1="866.46" x2="745.01" y2="866.46" style="fill:none;stroke:#ff5bae;stroke-linejoin:round;stroke-width:2px"></line><path class="annotation annotations1" d="M441.37,832.68V734.9a10,10,0,0,1,10-10h368a10,10,0,0,1,10,10v97" transform="translate(-45 -62.4)" style="fill:none;stroke:#ff5bae;stroke-miterlimit:10;stroke-width:8px"></path></g></svg>
  <figcaption>The first couple of bars, in score form, of the funk groove used in this blog post. Explanatory annotations are in pink.</figcaption>
</figure>
<p>The tutorials feature a <a href="https://en.wikipedia.org/wiki/Chord_chart">musical chart</a> (score), often with explanatory annotations, for each example. The site is aimed mostly at players who already have at least some experience of playing piano, and such folk can probably already read Western music notation. To these users the animated keyboards may have limited value.</p>
<p>Even if you <em>can</em> read music, however, seeing the velocities indicated visually might he helpful if you’re looking to improve your command of the jazz or funk ‘feel’. For this funk groove, for example, the colours could help users see which notes are being <a href="https://music.stackexchange.com/questions/2115/what-is-a-ghost-note/2116#2116">‘ghosted’</a>.</p>
<h2 id="technical-details">Technical details</h2>
<h3 id="playing-audio-when-an-ios-device-is-muted">Playing audio when an iOS device is muted</h3>
<p>On iOS, the Web Audio API can’t produce sound if the device is muted. This is probably a feature (rather than a bug) to prevent websites from playing sound when the user doesn’t expect it.</p>
<p>In the case of JazzKeys.fyi, users are explicitly tapping a ‘Play’ button and so very likely want to hear something even if their mute switch is active.</p>
<p>Unlike the Web Audio API, the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio">HTML5 <code>&lt;audio&gt;</code> element</a> plays sound no matter a device’s mute state, and so we can work around the no-sound-on-mute issue by simultaneously playing a <a href="https://github.com/donbrae/onscreen-piano-keyboard/blob/main/src/index.js#L160-L179">silent audio file</a> via an <code>&lt;audio&gt;</code> element.</p>
<h3 id="code-structure">Code structure</h3>
<p>In a typical JazzKeys.fyi tutorial, there will be several musical examples. I structured this in code by creating the class <code>ItemAudioState</code>, from which a new object per example is <a href="https://github.com/donbrae/onscreen-piano-keyboard/blob/main/src/index.js#L91-L105">instantiated</a> on page load:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">class</span> <span class="token class-name">ItemAudioState</span> <span class="token punctuation">{</span><br>  <span class="token function">constructor</span><span class="token punctuation">(</span><br>    <span class="token parameter">name<span class="token punctuation">,</span><br>    buttonPlay<span class="token punctuation">,</span><br>    loop<span class="token punctuation">,</span><br>    tempo<span class="token punctuation">,</span><br>    filePath<span class="token punctuation">,</span><br>    midi<span class="token punctuation">,</span><br>    numberOfKeys</span><br>  <span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span>name <span class="token operator">=</span> name<span class="token punctuation">;</span> <span class="token comment">// e.g. lick-blues-1. Taken from `data-name` on .button-play</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span>buttonPlay <span class="token operator">=</span> buttonPlay<span class="token punctuation">;</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span>loop <span class="token operator">=</span> loop<span class="token punctuation">;</span> <span class="token comment">// true/false</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span>tempo <span class="token operator">=</span> tempo<span class="token punctuation">;</span> <span class="token comment">// integer/undefined</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span>filePath <span class="token operator">=</span> filePath<span class="token punctuation">;</span> <span class="token comment">// e.g. /audio/lick-blues-1/lick-blues-1-straight-120 (file extensions added later)</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span>midi <span class="token operator">=</span> midi<span class="token punctuation">;</span> <span class="token comment">// Path to MIDI file</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span>numberOfKeys <span class="token operator">=</span> numberOfKeys<span class="token punctuation">;</span> <span class="token comment">// Number of keys on keyboard</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<h3 id="state-management">State management</h3>
<p>Each instantiated instance of <code>ItemAudioState</code> is stored in the <code>state</code> object, in the <code>itemsState</code> property:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token keyword">const</span> state <span class="token operator">=</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">  <span class="token literal-property property">init</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span></span><br><mark class="highlight-line highlight-line-active">  <span class="token literal-property property">itemsState</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token comment">// Instantiated `ItemAudioState` objects</span></mark><br><span class="highlight-line">  <span class="token literal-property property">activeItemState</span><span class="token operator">:</span> <span class="token keyword">undefined</span><span class="token punctuation">,</span> <span class="token comment">// Reference to active item's object in `itemsState`. Active item == currently playing item (set on Play button click)</span></span><br><span class="highlight-line">  <span class="token literal-property property">audio</span><span class="token operator">:</span> <span class="token keyword">undefined</span><span class="token punctuation">,</span> <span class="token comment">// Main howler object</span></span><br><span class="highlight-line">  <span class="token literal-property property">playing</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span></span><br><span class="highlight-line">  <span class="token literal-property property">loading</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span></span><br><span class="highlight-line">  <span class="token literal-property property">playingItemObjectName</span><span class="token operator">:</span> <span class="token keyword">undefined</span><span class="token punctuation">,</span> <span class="token comment">// Name of the item currently being played</span></span><br><span class="highlight-line">  <span class="token literal-property property">playCount</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token comment">// Incremented after end of each play of main audio file</span></span><br><span class="highlight-line">  <span class="token literal-property property">updateAudio</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token comment">// Was a relevant parameter (e.g. tempo or rhythm) changed during playing that requires the main audio file playing to be updated?</span></span><br><span class="highlight-line">  <span class="token literal-property property">isiOS</span><span class="token operator">:</span> <span class="token keyword">undefined</span><span class="token punctuation">,</span> <span class="token comment">// true/false</span></span><br><span class="highlight-line">  <span class="token literal-property property">htmlAudio</span><span class="token operator">:</span> <span class="token keyword">undefined</span><span class="token punctuation">,</span> <span class="token comment">// For iOS</span></span><br><span class="highlight-line">  <span class="token literal-property property">htmlAudioSetup</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token comment">// Flag. Background HTML5 element created</span></span><br><span class="highlight-line">  <span class="token literal-property property">htmlAudioPlaying</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token comment">// Flag. Background HTML5 has started playing</span></span><br><span class="highlight-line">  <span class="token literal-property property">tonejs</span><span class="token operator">:</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">    <span class="token literal-property property">itemsMidi</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token comment">// MIDI files as JS objects</span></span><br><span class="highlight-line">    <span class="token literal-property property">visuals</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span></span><br><span class="highlight-line">  <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br><span class="highlight-line">  <span class="token literal-property property">timeouts</span><span class="token operator">:</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">    <span class="token comment">// Store timeouts in state object so they can be cleared (and we can prevent multiple instances thereof being fired). All timeouts in this object are cleared by playStopHowl()</span></span><br><span class="highlight-line">    <span class="token literal-property property">loading</span><span class="token operator">:</span> <span class="token keyword">undefined</span> <span class="token comment">// If audio taking more than a brief moment to load, show 'loading' indicator</span></span><br><span class="highlight-line">  <span class="token punctuation">}</span></span><br><span class="highlight-line"><span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
<p>Listeners are added to various UI elements, such as the <a href="https://github.com/donbrae/onscreen-piano-keyboard/blob/main/src/index.js#L448-L458">‘Loop’</a> switch and <a href="https://github.com/donbrae/onscreen-piano-keyboard/blob/main/src/index.js#L439-L446">‘Tempo’</a> dropdown menu. When there is a change in value to one of those UI elements, a matching property in the relevant object in <code>state.itemsState</code> is updated.</p>
<p>A listener is also <a href="https://github.com/donbrae/onscreen-piano-keyboard/blob/main/src/index.js#L460-L461">added to the <code>body</code></a> to capture any change that‘s made in the UI. This updates the audio file path based on changes to tempo and rhythm (whose values themselves are updated by the element-specific listeners described in the previous paragraph). It also captures changes to elements added dynamically, such as the ‘Bar dimming’ switch<sup class="footnote-ref"><a href="#fn2" id="fnref2">2</a></sup>.</p>
<h4 id="querying-the-state">Querying the state</h4>
<p>The state can be updated at any time, including while audio is being played and the keyboard is animating. The updated state will then be queried at the relevant point in the code, such as when the program decides whether the audio should <a href="https://github.com/donbrae/onscreen-piano-keyboard/blob/main/src/index.js#L303-L314">loop and/or load new audio</a>.</p>
<p>The state management also handles the scenario in which the user interrupts the playing of one example by clicking ‘Play’ on another example. Here, the object in <code>state.itemsState</code> which corresponds to the newly activated example will become the source of what audio is to play and what keyboard is to animate.</p>
<h4 id="html-data-attributes">HTML data attributes</h4>
<p>The <code>id</code> and various  <code>data</code> attributes in the HTML associate the ‘Play’ button with its accompanying UI elements:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>flex controls<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br><mark class="highlight-line highlight-line-active">  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>button button-play<span class="token punctuation">"</span></span> <span class="token attr-name">data-name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>figure-bebop-6<span class="token punctuation">"</span></span> <span class="token attr-name">data-tempo</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#tempo6<span class="token punctuation">"</span></span> <span class="token attr-name">data-chord_name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#chord6<span class="token punctuation">"</span></span> <span class="token attr-name">data-loop</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#loop6<span class="token punctuation">"</span></span> <span class="token attr-name">data-annotations</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#annotations6<span class="token punctuation">"</span></span> <span class="token attr-name">data-midi</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>88|49<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Play<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span></mark><br><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></span><br><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>flex controls<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br><span class="highlight-line">  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>tempo6<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Tempo (speed)<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span></span><br><mark class="highlight-line highlight-line-active">    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>select</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>tempo6<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>select tempo<span class="token punctuation">"</span></span> <span class="token attr-name">data-name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>figure-bebop-6<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></mark><br><span class="highlight-line">      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span> <span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>140<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>140bpm (1x)<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span> <span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>105<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>105bpm (0.75x)<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span> <span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>70<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>70bpm (0.5x)<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>select</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>chord6<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Chords<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span></span><br><mark class="highlight-line highlight-line-active">    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>select</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>chord6<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>select chord-name<span class="token punctuation">"</span></span> <span class="token attr-name">data-name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>figure-bebop-6<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></mark><br><span class="highlight-line">      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span> <span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token punctuation">"</span></span><span class="token punctuation">></span></span>None<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span> <span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>C7|F-rh-140<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>C7 | Fmaj7<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span> <span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Gm7-C7|F-rh-140<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Gm7 C7 | Fmaj7<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>select</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>form-switch<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br><span class="highlight-line">      Loop</span><br><mark class="highlight-line highlight-line-active">      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>checkbox<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>loop<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>loop6<span class="token punctuation">"</span></span> <span class="token attr-name">data-name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>figure-bebop-6<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></mark><br><span class="highlight-line">      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>i</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>i</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>form-switch<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br><span class="highlight-line">      Annotations</span><br><mark class="highlight-line highlight-line-active">      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>checkbox<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>annotations<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>annotations6<span class="token punctuation">"</span></span> <span class="token attr-name">checked</span> <span class="token attr-name">data-name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>figure-bebop-6<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></mark><br><span class="highlight-line">      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>i</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>i</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span></span><br><span class="highlight-line">  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></span><br><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></span></code></pre>
<p>That all elements for each example share the same <code>name</code> data attribute allows the JavaScript to target the relevant object in state.<sup class="footnote-ref"><a href="#fn3" id="fnref3">3</a></sup></p>
<h2 id="get-notified-on-launch">Get notified on launch</h2>
<p>And there we have it: a simple animated piano keyboard built in JavaScript. You can view the demo and edit its code at <a href="https://codesandbox.io/s/onscreen-piano-keyboard-3uhrm">this CodeSandbox</a>.</p>
<p>If you’d like to be notified when JazzKeys.fyi launches, feel free to <a href="https://www.jazzkeys.fyi/#heading-get-notified-on-launch">enter your email</a>.</p>
<hr>
<p class="footnotes">Related post: <a href="/posts/logic-ascore-font/">Fix for missing ‘AScore’ font when editing Logic Pro PDFs</a>.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<h2 class="h5">Footnotes</h2>
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Different hues of the same colour alone may not be enough to make velocity clear to the user. I’m considering adding a line similar to that featured in <a href="https://support.apple.com/en-gb/guide/logicpro/lgcpa8fee137/mac">Logic Pro’s piano roll editor</a>. Logic also uses colour to describe velocity, but following the Logic approach of using different colours would, in my case, confuse which hand is playing. I’ve also experimented with modifying the radius of the circles above the keys to indicate velocity. I may finesse this feature and activate it in the future. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>The ‘bar dimming’ feature highlights the currently playing bar in the musical score to help the user follow along. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p><code>data-midi=&quot;88|49&quot;</code> means that a 49-key version of the SVG keyboard can be shown on narrower devices (such as phones) if the example in question contains no notes outwith the 49-key range. This improves the viewing experience for mobile users. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>All* my macOS keyboard shortcuts</title>
      <link href="https://www.jamieonkeys.dev/posts/keyboard-shortcuts/"/>
      <updated>2022-03-31T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/keyboard-shortcuts/</id>
      <content type="html">
        <![CDATA[
      <p>I use macOS to <a href="/projects/#heading-web">build websites</a> and <a href="/projects/#heading-music">compose music</a>. This post is a  collection of keyboard shortcuts and modifiers that help my day-to-day productivity.</p>
<h2 id="macos-native-shortcuts-and-modifiers">macOS native shortcuts and modifiers</h2>
<p>Useful shortcuts already defined by the system.</p>
<h3 id="window-management">Window management</h3>
<ul>
<li><kbd>Command</kbd>-<kbd>Backtick</kbd>: cycle current application’s windows</li>
<li><kbd>Command</kbd>-<kbd>H</kbd>: hide active application’s windows<sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup></li>
<li><kbd>Command</kbd>-<kbd>Option</kbd>-<kbd>H</kbd>: hide all windows expect those belonging to the active application’s<sup class="footnote-ref"><a href="#fn1" id="fnref1:1">1:1</a></sup></li>
<li>Hold <kbd>Option</kbd> while resizing window: change window dimension(s)<sup class="footnote-ref"><a href="#fn2" id="fnref2">2</a></sup> around the window’s current centre point</li>
<li>Hold <kbd>Option</kbd>-<kbd>Shift</kbd> while resizing window: change window dimensions around the window’s current centre point while maintaining aspect ratio</li>
</ul>
<p>(See also <a href="#heading-rectangle-pro">Rectangle Pro</a>.)</p>
<h3 id="special-characters">Special characters</h3>
<ul>
<li>Hold <kbd>$KEY</kbd> in text fields to get pop-up menu of given character with diacritic markings</li>
<li><kbd>Control</kbd>-<kbd>Command</kbd>-<kbd>Space bar</kbd>: open <a href="https://support.apple.com/en-gb/guide/mac-help/mchlp1560/mac">Character Viewer</a> (emoji and symbols picker)</li>
<li><kbd>Option</kbd>-<kbd>Hyphen</kbd>: insert en dash (–)</li>
<li><kbd>Option</kbd>-<kbd>Shift</kbd>-<kbd>Hyphen</kbd>: em dash (—)</li>
<li><kbd>Option</kbd>-<kbd>Close square bracket</kbd>: opening single curly quote (‘)</li>
<li><kbd>Option</kbd>-<kbd>Shift</kbd>-<kbd>Close square bracket</kbd>: closing single curly quote (’)</li>
<li><kbd>Option</kbd>-<kbd>Open square bracket</kbd>: opening double curly quote (“)</li>
<li><kbd>Option</kbd>-<kbd>Shift</kbd>-<kbd>Open square bracket</kbd>: closing single curly quote (”)</li>
</ul>
<figure>
	<video width="1400" height="876" muted loop controls controlslist="nodownload nofullscreen">
		<source src="/img/special-characters.mp4" type="video/mp4">
		Your browser doesn’t support HTML5 video tag.
	</video>
	<figcaption>Keyboard shortcuts for various special characters.</figcaption>
</figure>
<p>You can also search the Character Viewer (<kbd>Control</kbd>-<kbd>Command</kbd>-<kbd>Space bar</kbd>) by name, for ‘en dash’ etc.</p>
<h3 id="%E2%80%98hidden%E2%80%99-functionality">‘Hidden’ functionality</h3>
<p>Holding the <kbd>Option</kbd> key while clicking or hovering over a UI element can reveal additional functionality or information. Some examples: the Wi-Fi and Bluetooth menubar icons; the <a href="https://support.apple.com/en-gb/guide/mac-help/mh15155/mac">‘Plus’ button when building a Finder search</a> (allowing you to enhance the search logic); and application dropdown menus in the menu bar.</p>
<p>One I find useful is ‘Copy as Pathname’ in the Finder Edit menu:</p>
<figure>
	<img src="/img/finder-menu-option.gif" width="700" height="417" alt="Press the Option key to reveal additional functionality, such as ‘Copy as Pathname’">
	<figcaption>Press the <kbd>Option</kbd> key to reveal additional functionality, such as ‘Copy as Pathname’ in the Finder Edit menu.</figcaption>
</figure>
<h2 id="custom-os-mappings">Custom OS mappings</h2>
<p>You can assign application keyboard shortcuts in System Preferences &gt; Keyboard &gt; Shortcuts &gt; App Shortcuts:</p>
<figure>
	<img src="/img/app-shortcuts.png" width="714" height="637" alt="Set application shortcuts in System Preferences">
	<figcaption>Set application shortcuts in System Preferences.</figcaption>
</figure>
<ul>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>Command</kbd>-<kbd>M</kbd> in Finder: run <em>Window</em> &gt; <em>Merge All Windows</em></li>
<li><kbd>Command</kbd>-<kbd>Option</kbd>-<kbd>I</kbd> in Preview: <em>Adjust Size…</em> from the Tools menu</li>
<li><kbd>Shift</kbd>-<kbd>Command</kbd>-<kbd>Forward slash</kbd> in all apps: ‘Show Help menu’ (to search menu items):</li>
</ul>
<figure>
	<video width="714" height="204" muted loop controls controlslist="nodownload">
		<source src="/img/search-menu-items.mp4" type="video/mp4">
		Your browser doesn’t support HTML5 video tag.
	</video>
	<figcaption>Use <kbd>Shift</kbd>-<kbd>Command</kbd>-<kbd>Forward slash</kbd> to search menu items. Particularly useful in apps like Adobe Illustrator, where there are tons of menu options.</figcaption>
</figure>
<h2 id="rectangle-pro">Rectangle Pro</h2>
<div class="mt-3"><a href="https://rectangleapp.com" class="inline-block"><img src="/img/rectangle-pro-icon.png" width="150" height="150" alt="Rectangle Pro logo" class="app-icon nae-shadow"></a></div>
<p class="mt-2"><a href="https://rectangleapp.com">Third-party window manager</a>. Includes <a href="https://support.microsoft.com/en-us/windows/snap-your-windows-885a9b1e-a983-a3b1-16cd-c531795e6241">window snapping</a> à la Microsoft Windows. The keyboard shortcuts I use:</p>
<ul>
<li><kbd>Shift</kbd>-<kbd>Command</kbd>-<kbd>Enter</kbd>: current window to medium size, centred (custom shortcut)</li>
<li><kbd>Command</kbd>-<kbd>Option</kbd>-<kbd>Enter</kbd>: ‘Almost Maximize’</li>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>Command</kbd>-<kbd>Enter</kbd>: full screen, with some padding, centred (custom shortcut)</li>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>Command</kbd>-<kbd>Left Arrow</kbd>: ‘Left Half’</li>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>Command</kbd>-<kbd>Right Arrow</kbd>: ‘Right Half’</li>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>Command</kbd>-<kbd>Down Arrow</kbd>: ‘Center’</li>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>Command</kbd>-<kbd>Escape</kbd>: ‘Restore’</li>
</ul>
<figure>
	<img src="/img/rectangle-pro-custom-shortcuts.jpg" width="714" height="598" alt="Custom shortcuts in Rectangle Pro.">
	<figcaption>Custom shortcuts in Rectangle Pro.</figcaption>
</figure>
<h2 id="karabiner-elements">Karabiner-Elements</h2>
<div class="mt-3"><a href="https://karabiner-elements.pqrs.org" class="inline-block"><img src="/img/karabiner-elements-icon.png" width="150" height="150" alt="Rectangle Pro logo" class="app-icon nae-shadow"></a></div>
<p class="mt-2"><a href="https://karabiner-elements.pqrs.org">Third-party keyboard customiser</a> that I use to open and switch between commonly used apps with a single keystroke. It’s generally more efficient than using a trackpad or mouse, Spotlight, <a href="https://obdev.at/products/launchbar/index.html">LaunchBar</a> or <kbd>Command</kbd>-<kbd>Tab</kbd>ing.</p>
<p>I generally assign <kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>$KEY</kbd> to these.</p>
<ul>
<li><kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>U</kbd>: launch/switch to Apple Music</li>
<li><kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>C</kbd>: Calendar</li>
<li><kbd>Caps Lock</kbd><sup class="footnote-ref"><a href="#fn3" id="fnref3">3</a></sup>: Finder</li>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>Shift</kbd>-<kbd>D</kbd>: Downloads folder in Finder</li>
<li><kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>F</kbd>: Firefox</li>
<li><kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>D</kbd>: Firefox Developer Edition</li>
<li><kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>G</kbd>: Google Chrome</li>
<li><kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>I</kbd>: iTerm</li>
<li><kbd>Shift</kbd>-<kbd>Caps Lock</kbd>: Logic Pro</li>
<li><kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>M</kbd>: Microsoft Teams</li>
<li><kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>N</kbd>: Nova</li>
<li><kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>S</kbd>: Safari</li>
<li><kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>P</kbd>: Spotify</li>
<li><kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>T</kbd>: Terminal</li>
<li><kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>R</kbd>: Transmit</li>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>Shift</kbd>-<kbd>V</kbd>: Visual Studio</li>
<li><kbd>Control</kbd>-<kbd>Shift</kbd>-<kbd>V</kbd>: Visual Studio Code</li>
</ul>
<p>Assign shortcuts by editing <code>profiles[0].simple_modifications</code> in ~/.config/karabiner/karabiner.json<sup class="footnote-ref"><a href="#fn4" id="fnref4">4</a></sup>. For example, to open Firefox Developer Edition:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>	<span class="token property">"from"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>		<span class="token property">"key_code"</span><span class="token operator">:</span> <span class="token string">"d"</span><span class="token punctuation">,</span><br>		<span class="token property">"modifiers"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>			<span class="token property">"mandatory"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>				<span class="token string">"left_shift"</span><span class="token punctuation">,</span><br>				<span class="token string">"left_control"</span><br>			<span class="token punctuation">]</span><br>		<span class="token punctuation">}</span><br>	<span class="token punctuation">}</span><span class="token punctuation">,</span><br>	<span class="token property">"to"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>		<span class="token punctuation">{</span><br>			<span class="token property">"shell_command"</span><span class="token operator">:</span> <span class="token string">"open /Applications/Firefox\\ Developer\\ Edition.app"</span><br>		<span class="token punctuation">}</span><br>	<span class="token punctuation">]</span><br><span class="token punctuation">}</span></code></pre>
<p>Or to open Finder (or whatever you have assigned as your default file browser):</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>	<span class="token property">"from"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>		<span class="token property">"key_code"</span><span class="token operator">:</span> <span class="token string">"caps_lock"</span><br>	<span class="token punctuation">}</span><span class="token punctuation">,</span><br>	<span class="token property">"to"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>		<span class="token punctuation">{</span><br>			<span class="token property">"consumer_key_code"</span><span class="token operator">:</span> <span class="token string">"al_local_machine_browser"</span><br>		<span class="token punctuation">}</span><br>	<span class="token punctuation">]</span><br><span class="token punctuation">}</span></code></pre>
<h3 id="open-several-finder-windows-at-specific-locations">Open several Finder windows at specific locations</h3>
<p>I use <kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>Shift</kbd>-<kbd>J</kbd> to open several Finder windows<sup class="footnote-ref"><a href="#fn5" id="fnref5">5</a></sup> at locations relevant to my <a href="https://www.jazzkeys.fyi">JazzKeys.fyi project</a>:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>	<span class="token property">"from"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>		<span class="token property">"key_code"</span><span class="token operator">:</span> <span class="token string">"j"</span><span class="token punctuation">,</span><br>		<span class="token property">"modifiers"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>			<span class="token property">"mandatory"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>				<span class="token string">"left_control"</span><span class="token punctuation">,</span><br>				<span class="token string">"left_shift"</span><span class="token punctuation">,</span><br>				<span class="token string">"left_option"</span><br>			<span class="token punctuation">]</span><br>		<span class="token punctuation">}</span><br>	<span class="token punctuation">}</span><span class="token punctuation">,</span><br>	<span class="token property">"to"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>		<span class="token punctuation">{</span><br>			<span class="token property">"shell_command"</span><span class="token operator">:</span> <span class="token string">"open ~/Documents/dev/JazzKeys.fyi\\ process\\ files\\ \\(Hazel\\); open ~/Documents/dev/jazztoolkit/site/audio; open ~/Library/Mobile\\ Documents/com~apple~CloudDocs/Logic\\ projects/jazzkeys.fyi/Jamming; open ~/Library/Mobile\\ Documents/com~apple~CloudDocs/Logic\\ projects/jazzkeys.fyi/Licks\\ etc."</span><br>		<span class="token punctuation">}</span><br>	<span class="token punctuation">]</span><br><span class="token punctuation">}</span></code></pre>
<h2 id="third-party-app-specific">Third-party app-specific</h2>
<p>Some third-party apps which I find useful enough to assign keyboard shortcuts to. The shortcuts are defined within the apps themselves.</p>
<p>I generally assign <kbd>Control</kbd>-<kbd>Option</kbd>(-<kbd>Command</kbd>)-<kbd>$KEY</kbd> to these.</p>
<ul>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>C</kbd>: open <a href="https://apprywhere.com/ce-mac.html">Copy ’Em</a> (clipboard manager)</li>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>F</kbd>: <a href="https://sindresorhus.com/dato">Dato</a><sup class="footnote-ref"><a href="#fn6" id="fnref6">6</a></sup> (menubar calendar app)</li>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>Command</kbd>-<kbd>Space</kbd>: <a href="https://devutils.app">DevUtils.app</a> (developer tools)</li>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>Command</kbd>-<kbd>F</kbd>: <a href="https://aptonic.com">Dropzone</a> (file management and general productivity booster)</li>
<li><kbd>Control</kbd>-<kbd>Z</kbd>: <a href="https://manytricks.com/menuwhere">Menuwhere</a> (get the Mac menu bar wherever your cursor is)</li>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>Command</kbd>-<kbd>N</kbd>: <a href="https://quicknoteapp.com">Quick Note</a> &gt; new note</li>
<li><kbd>Shift</kbd>-<kbd>Command</kbd>-<kbd>2</kbd>: <a href="http://snappy-app.com/download">Snappy</a> (take screenshots which remain in front of other windows) &gt; new snap</li>
<li><kbd>Control</kbd>-<kbd>Option</kbd>-<kbd>Command</kbd>-<kbd>C</kbd>: <a href="https://sindresorhus.com/system-color-picker">System Color Picker</a> (OS-wide access to an enhanced system colour picker) &gt; pick colour</li>
</ul>
<p>(I also use <a href="https://www.trankynam.com/atext">aText</a> for text automation/expansion, but that’s probably a separate blog post.)</p>
<hr class="footnotes-sep">
<section class="footnotes">
<h2 class="h5">Footnotes</h2>
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Useful for decluttering your workspace. <a href="#fnref1" class="footnote-backref">↩︎</a> <a href="#fnref1:1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>It depends on the position of the cursor on the window edge as to whether the X, Y or both X and Y dimensions are affected. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>I first disabled the Caps Lock key for its default purpose: search ‘Reset modifier keys’ in System Preferences and set ‘Caps Lock (⇪) Key’ to <em>No Action</em>. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>This location is accessible via Karabiner-Elements preferences under Misc &gt; Export &amp; Import &gt; Open config folder […]. <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p>If you hold down <kbd>Command</kbd> while positioning the Finder windows, the OS should remember the coordinates. <a href="#fnref5" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn6" class="footnote-item"><p><kbd>F</kbd> because I used to use Fantastical. <a href="#fnref6" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Tripods: an HTML5 puzzle game</title>
      <link href="https://www.jamieonkeys.dev/posts/tripods-html5-game/"/>
      <updated>2021-05-31T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/tripods-html5-game/</id>
      <content type="html">
        <![CDATA[
      <p>At the turn of the year I set out to finish <a href="https://www.tripodsgame.com">Tripods</a>, a game I started making back in 2013. It’s a 2D puzzler built for the web using the DOM and SVG.<sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup></p>
<figure>
    <img src="/img/tripods.jpg" width="747" height="481" alt="Screenshot of level 2 of Tripods">
    <figcaption>Screenshot of level 2 of Tripods.</figcaption>
</figure>
<p>I wrote the original prototype in 2013 using jQuery, but it’s now 2021 so the first task was to refactor the codebase to use vanilla JavaScript.<sup class="footnote-ref"><a href="#fn2" id="fnref2">2</a></sup> For the animations — previously handled by jQuery’s <code>animate()</code> function — I ended up using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API">Web Animations API</a>. It’s still at Working Draft status, but <a href="https://caniuse.com/web-animation">widely supported</a>. I actually began by using the <code>top</code>/<code>left</code> position properties to move elements, and specifying the animation transitions in the CSS, but the frame rate across devices was patchy. The <a href="https://github.com/donbrae/tripods-web/commit/49f17ad9f6f934099348c2095d97cd84abebf7e0#diff-0f456c29dad5196b66a1f8fb83aa744b8a20e7aa0ce70a77f0a4ada31b497c42L623-R632">switch</a> to using the WAAPI and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/translate()"><code>translate()</code></a> CSS function — which leverages <a href="https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/">hardware acceleration</a> — resulted in improved performance and more fine-grained control of animations.</p>
<p>Here’s a basic example of the WAAPI and <code>translate()</code> in action<sup class="footnote-ref"><a href="#fn3" id="fnref3">3</a></sup>:</p>
<p class="codepen" data-height="450" data-theme-id="dark" data-default-tab="js,result" data-user="donbrae" data-slug-hash="XWNpOgq" style="height: 450px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Web Animations API">
  <span>See the Pen <a href="https://codepen.io/donbrae/pen/XWNpOgq">
  Web Animations API</a> by donbrae (<a href="https://codepen.io/donbrae">@donbrae</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/
embed/ei.js"></script>
<p>The <a href="https://howlerjs.com">howler.js</a> library is used for audio<sup class="footnote-ref"><a href="#fn4" id="fnref4">4</a></sup>, and <a href="https://github.com/catdad/canvas-confetti">canvas-confetti</a> for the ‘win’ animation.</p>
<p>The game works with both pointer and touch input, and can more or less function as a Progressive Web App (PWA) on mobile devices by way of <a href="https://github.com/donbrae/tripods-web/blob/08649700a5dcb318c87662e8204cf180de4d7838/index.html#L10-L12">iOS-specific metatags</a>, <a href="https://www.ionos.co.uk/tools/favicon-generator">favicons</a>, <a href="https://appsco.pe/developer/splash-screens">splash screen graphics</a> (iOS only), and a <a href="https://github.com/donbrae/tripods-web/blob/main/dist/manifest.json">manifest.json</a> file. (Offline access still needs to be implemented.) When added to the home screen on my iPhone it pretty much behaves as if it was a native app.</p>
<p>I built it mainly using <a href="https://code.visualstudio.com">VS Code</a> and <a href="https://www.mozilla.org/en-GB/firefox/developer">Firefox Developer Edition</a>, with <a href="https://github.com">GitHub</a> for version control and <a href="https://www.netlify.com">Netlify</a> for hosting and easy git-based deployment. (Oh, and I created the sounds in <a href="https://www.apple.com/uk/logic-pro">Logic</a>.)</p>
<figure>
    <img src="/img/tripods-original-2013-sketch.jpg" width="747" height="503" alt="The original design sketch I made of the Tripods idea in 2013">
    <figcaption>The original design sketch I made of the Tripods idea in 2013.</figcaption>
</figure>
<p>Overall, this was a challenging and enjoyable side project. I started my programming journey writing games in Sinclair BASIC, so the process has had nostalgia value alongside giving me a chance to hone my dev chops. For anyone who’s interested, the <a href="https://github.com/donbrae/tripods-web">code is available to view on GitHub</a>, and you can <a href="https://www.tripodsgame.com">play the game here</a>.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<h2 class="h5">Footnotes</h2>
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Web games are typically written to the Canvas or WebGL APIs, but I think the DOM is well enough suited to building a simple grid-based puzzle. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Unlike in 2013, today you get much of the convenience of jQuery built into enough browsers that there is <a href="http://youmightnotneedjquery.com">much less need to use it</a>. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>Internet Explo— I mean Safari, apparently can’t show a drop-shadow simultaneously with a scale transform, and in the process also cancels the blur. The actual game therefore doesn’t feature a drop-shadow; motion blur won priority in that trade-off. The CodePen here, though, is the full-fat effect, so it runs best in any browser that’s not Safari. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>I opted for howler.js over the plain Web Audio API as I wanted to ship a 1.0 as soon as possible, and I’d already used it on <a href="https://www.jazzkeys.fyi">another project</a>. <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Preventing zooming on mobile</title>
      <link href="https://www.jamieonkeys.dev/posts/preventing-zooming-on-mobile/"/>
      <updated>2021-02-05T00:00:00+00:00</updated>
      <id>https://www.jamieonkeys.dev/posts/preventing-zooming-on-mobile/</id>
      <content type="html">
        <![CDATA[
      <p>I’m creating an HTML5 puzzle game and need to prevent mobile users from zooming in, either via double-tapping or pinching. Normally, for accessibility reasons, you’d not want to have this restriction. But in terms of the game it was making the user experience worse and even messing up geometric calculations the game makes to detect a ‘win’.</p>
<p>If memory serves, what used to be sufficient was adding <code>user-scalable=no</code> and  <code>maximum-scale=1</code> to the ‘viewport’ meta tag:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>viewport<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre>
<p>However this certainly doesn’t work on iOS now, and using it will <a href="https://web.dev/meta-viewport/">lower your Google Lighthouse score</a>.</p>
<p>It is nonetheless still possible to disable zooming.</p>
<p><code>touch-action: none;</code>  prevents pinching to zoom (source: <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action#disabling_all_gestures">MDN</a>):</p>
<pre class="language-css"><code class="language-css"><span class="token selector">#game_surface</span> <span class="token punctuation">{</span><br>	<span class="token property">touch-action</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>(I’ve actually applied this rule to <code>html</code> and <code>body</code>, rather than just the game surface.)</p>
<p>And you can disable double-tap-to-zoom with JavaScript (source: <a href="https://stackoverflow.com/questions/37808180/disable-viewport-zooming-ios-10-safari/38573198#38573198">Stack Overflow</a>):</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">let</span> last_touch_end <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span><br>document<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"touchend"</span><span class="token punctuation">,</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> now <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>now <span class="token operator">-</span> last_touch_end <span class="token operator">&lt;=</span> <span class="token number">300</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      e<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br>  last_touch_end <span class="token operator">=</span> now<span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>You might also wish to disable text selection:</p>
<pre class="language-css"><code class="language-css"><span class="highlight-line"><span class="token selector">#game_surface</span> <span class="token punctuation">{</span></span><br><span class="highlight-line">	<span class="token property">touch-action</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span></span><br><mark class="highlight-line highlight-line-active">    <span class="token property">-webkit-user-select</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span></mark><br><mark class="highlight-line highlight-line-active">    <span class="token property">user-select</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span></mark><br><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>It’s worth repeating that you shouldn’t add these restrictions to web pages and apps by default, as it compromises accessibility. But for a game or similar application where you do want to prevent default zooming, the above code does the job.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>New side project landing page</title>
      <link href="https://www.jamieonkeys.dev/posts/jazzkeysfyi/"/>
      <updated>2020-11-18T00:00:00+00:00</updated>
      <id>https://www.jamieonkeys.dev/posts/jazzkeysfyi/</id>
      <content type="html">
        <![CDATA[
      <p>I’ve been working on a new side project of late, and its <a href="https://www.jazzkeys.fyi">landing page is now live</a>. I’ve described it as a ‘toolkit’ for jazz keyboard players. Basically it’s a bunch of patterns, licks, philosophies and techniques I’ve acquired over the twenty-or-so years I’ve been playing jazz.</p>
<p>I remember earlier on in my musical journey thinking it would be great if I could just find a ready collection of blues licks to quickly ‘level up’ my playing (my command of blues was seriously lacking at that point). Same thing when I later found myself in a funk band and knowing not much about playing funk keyboards. I also long struggled to sound ‘modern’ and find my own contemporary jazz voice.</p>
<p>This new site aims to be a resource for other players out there who may be struggling in some way or who just want to augment their playing. As per the landing page, there will be written scores for those that can read Western music notation; and audio too, with the option to slow down and loop the audio, as well as listen with chordal accompaniment.</p>
<p>Like this blog, it’s a Jamstack site built with the <a href="https://github.com/MadeByMike/supermaya">Supermaya</a> <a href="https://www.11ty.dev">Eleventy</a> starter kit, and is hosted on <a href="https://www.netlify.com">Netlify</a>. The <a href="https://howlerjs.com">howler.js</a> library is used to make working with the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API">Web Audio API</a> less of a <a href="https://scots.app/q/fash">fash</a>.</p>
<p><a href="https://www.jazzkeys.fyi">Check it out</a>, and add your email if you’d like to be notified when it launches proper.</p>
<p><strong>Update</strong>: I’ve published a tutorial on <a href="https://www.jazzkeys.fyi/bebop-enclosures/">bebop enclosures</a>.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Is iOS?</title>
      <link href="https://www.jamieonkeys.dev/posts/is-ios/"/>
      <updated>2020-11-11T00:00:00+00:00</updated>
      <id>https://www.jamieonkeys.dev/posts/is-ios/</id>
      <content type="html">
        <![CDATA[
      <p>I’m working on a side project that uses the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API">Web Audio API</a>. However on iOS the Web Audio API can’t produce a sound if a device is muted. (I think this is a feature rather than a bug.) <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio">HTML 5 Audio</a> plays sound no matter a device’s mute state, but you don’t get the same level of programmatic control that Web Audio affords.</p>
<p>A workaround is to use Web Audio but simultaneously play a silent audio file via HTML5 Audio.</p>
<p>This can be the subject of a future post. But first I needed to work out whether the user is running iOS. It’s simple enough, complicated only slightly by the fact that iPads run a version of desktop Safari. Here’s the code I’m using:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> user_agent <span class="token operator">=</span> navigator<span class="token punctuation">.</span>userAgent<span class="token punctuation">.</span><span class="token function">toLowerCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> is_iOS <span class="token operator">=</span><br>    user_agent<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">"iphone"</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span> <span class="token operator">||</span><br>    user_agent<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">"ipod"</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span> <span class="token operator">||</span><br>    user_agent<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">"ipad"</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span> <span class="token operator">||</span> <span class="token comment">// This may not be required</span><br>    <span class="token punctuation">(</span>navigator<span class="token punctuation">.</span>maxTouchPoints <span class="token operator">&amp;&amp;</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">Mac</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">.</span><span class="token function">test</span><span class="token punctuation">(</span>navigator<span class="token punctuation">.</span>platform<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// iPad running 'desktop' Safari</span></code></pre>
<p><code>is_iOS</code> returns truthy or falsey, letting us know whether the user is on iOS.</p>
<p>Good practice for tailoring your code to a particular browser is to use feature detection (as we do here with <code>navigator.maxTouchPoints</code>). However additionally querying <code>navigator.userAgent</code> for the presence of a string is fine too when we can’t rely soley on feature detection. Of course you may need to update the query in future if user agent or platform names change.</p>
<p>(An alternative to the playing-silent-HTML5-Audio workaround would be to instead read <code>is_iOS</code>’s value and print a helpful ‘Unmute your device’ message under the ‘Play’ button. Unfortunately there’s no API call you can make to get the mute state.)</p>
<p>You can <a href="https://w1ecw.csb.app">run the iOS detection code here</a> (courtesy of CodeSandbox).</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Fuse.js: JSON vs JS object literal speed tests</title>
      <link href="https://www.jamieonkeys.dev/posts/fusejs-json-object-literal-speed-tests/"/>
      <updated>2020-09-23T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/fusejs-json-object-literal-speed-tests/</id>
      <content type="html">
        <![CDATA[
      <p>I’m building a <a href="https://scots.app">Scots glossary app</a> using <a href="https://fusejs.io">Fuse.js</a> as the fuzzy-search algorithm.</p>
<p>The data is currently in the form of a <a href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Basics">JavaScript object literal</a>, and I wanted to check whether feeding Fuse <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON">JSON</a> instead would result in faster searches (as I <a href="https://scots.app/q/jaloused">jaloused</a> it might).</p>
<p>Fuse.js is already plenty fast but I wanted to identify further opportunites for optimisation as I (slowly) add more words to the dictionary. (And why not make the app as efficient as possible anyway?)</p>
<p><a href="https://github.com/donbrae/glossar/blob/0e99db45569745cabc611619051e2656650a7e7a/app.js#L164-L168">So I ran a few simple tests</a> on three desktop browsers. Each test does 300 searches (via the ‘choose a random word’ button) per browser per JS object literal and JSON file, the latter of which is 134 kB. The maximum number of possible values to check against in the dataset is 4,239.</p>
<p>The results, averaged across each of the six tests, are:</p>
<table>
<thead>
<tr>
<th></th>
<th>Chrome</th>
<th>Safari</th>
<th>Firefox</th>
</tr>
</thead>
<tbody>
<tr>
<td>Object literal</td>
<td>2.527ms</td>
<td>5.976ms</td>
<td>6.77ms</td>
</tr>
<tr>
<td>JSON</td>
<td>2.593ms</td>
<td>6.64ms</td>
<td>7.023ms</td>
</tr>
<tr>
<td>Object-literal-to-JSON difference (a positive result means slower)</td>
<td>+2.578%</td>
<td>+10.526%</td>
<td>+3.669%</td>
</tr>
</tbody>
</table>
<p>So I was wrong in my hypothesis: searching the JSON is in fact slower, by as much as 10% in the case of Safari.</p>
<p>As far as comparing the browsers, in these tests the Chrome V8 JavaScript engine performs about two-and-a-half times as fast as Safari and Firefox’s.</p>
<p>In terms of actually parsing larger amounts of data, <a href="https://v8.dev/blog/cost-of-javascript-2019#json">this article</a> pegs JSON as the winner, but the above tests show that for my use case — using Fuse.js to fuzzy-search a dataset with about 4,000 possible items to check against — object literals appear to be the way to go.</p>
<p>(There’s a <a href="https://codesandbox.io/s/fusejs-json-vs-object-literal-d9lmd">CodeSandbox here</a> with the actual results data.)</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Real-time RSS news aggregator</title>
      <link href="https://www.jamieonkeys.dev/posts/wrestle-buzz-overview/"/>
      <updated>2020-09-04T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/wrestle-buzz-overview/</id>
      <content type="html">
        <![CDATA[
      <p><a href="https://wrestle.buzz">wrestle.buzz</a> is a PHP and JavaScript web app I built that presents a ‘river’ of pro wrestling news, inspired by sites like <a href="https://www.techmeme.com">Techmeme</a> and <a href="http://nbariver.com">NBA River</a>. It uses <a href="https://simplepie.org">SimplePie</a> as the RSS feed parser, and Google’s natural language API <a href="https://cloud.google.com/natural-language/docs/reference/rest/v1/documents/analyzeEntities">analyzeEntities</a> method to parse news headlines and generate a real-time trending-topic list. (You can use a simple curl command to <a href="https://googleapis.github.io/HowToREST.html">test the API for yourself</a>.)</p>
<p>To provide a real-time experience the app checks the Google API and RSS output asynchronously at regular intervals: the user is notified in a button that there are ‘x new updates’. It aims to be similar to the Guardian’s <a href="https://www.theguardian.com/politics/live/2020/sep/03/uk-coronavirus-live-quarantine-portugal-covid-19-latest-news">‘Live’</a> pages.</p>
<video width="374" height="809" autoplay muted loop class="iphone-portrait" onclick="this.paused ? this.play() : this.pause();">
    <source src="/img/wrestle.buzz-new-items.mp4"
            type="video/mp4">
    <img width="374" height="809" src="/img/wrestle.buzz-new-items.jpg" class="iphone-portrait" alt="Screenshot of wrestle.buzz showing button notification that new stories are available">
</video>
<p>For me it’s a great way to keep up with what’s happening in the frequently wacky world of pro wrestling. It got some <a href="https://www.reddit.com/r/SquaredCircle/comments/hqn86e/i_built_a_realtime_pro_wrestling_news_aggregator/">good feedback</a> on r/SquaredCircle, too.</p>
<p>On a technical level, while it’s not the most complex app in the world I thought I should nonetheless create a high-level ‘software architecture’ flowchart before I forget how it all works. I used the free tool <a href="https://excalidraw.com">Excalidraw</a> to create it. PNG below; <a href="/img/wrestle.buzz-overview.svg">SVG here</a>.</p>
<figure>
    <img src="/img/wrestle.buzz-overview.png" width="747" height="542" alt="Flow-chart overview of wrestle.buzz" class="nae-shadow img-transparent">
    <figcaption>Flow-chart overview of wrestle.buzz.</figcaption>
</figure>
<p>I have a couple of updates in the planning so I can refer back to this to keep my bearings.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Running PHP and shell scripts as cron jobs</title>
      <link href="https://www.jamieonkeys.dev/posts/running-scripts-as-cron-jobs/"/>
      <updated>2020-09-03T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/running-scripts-as-cron-jobs/</id>
      <content type="html">
        <![CDATA[
      <p>Cron jobs are commands or scripts that run on a web server automatically at set intervals (every 15 minutes, once a day, twice a month etc.) My site <a href="https://wrestle.buzz">wrestle.buzz</a> uses a few of them (including to run an API call and clear the cache). I thought I’d document the process of setting one up.</p>
<h2 id="primer">Primer</h2>
<p>You can add cron jobs directly in a file called <code>crontab</code> (<a href="https://www.digitalocean.com/community/tutorials/how-to-use-cron-to-automate-tasks-ubuntu-1804">here’s an example doing that on Ubuntu</a>), or <a href="https://documentation.cpanel.net/display/74Docs/Cron+Jobs">via cPanel</a>.</p>
<p>Scripts, whether they’re shell (.sh) or PHP, need 755 (execute) permissions to run (<code>chmod 755 my_script.sh</code>).</p>
<p>Additionally absolute paths (e.g. <code>/home/user123/www/my.site/scripts/my_script.sh</code>) should be used when calling scripts.</p>
<p>By default you’ll get an email each time a cron job runs. You can alternatively output to <code>/dev/null</code> to avoid getting those emails. I chose to output to a (non-null) log file. Any errors in the execution of your script will be written to this file, so it’s useful in keeping an eye out for problems. If the cron job is successful the output file should be zero bytes.</p>
<h2 id="example">Example</h2>
<p>One of the shell scripts I run via cron is called <code>delete_cache.sh</code>. It deletes files in the cache (let’s call them <code>.tmp</code> files here) that were created over five days previous. It looks something like this:</p>
<pre class="language-shell"><code class="language-shell"><span class="token shebang important">#!/bin/bash</span><br><span class="token builtin class-name">shopt</span> -s extglob<br><br><span class="token function">find</span> /home/user123/www/my.site/cache -type f -mtime +5 -name <span class="token string">'*.tmp'</span> -exec <span class="token function">rm</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token punctuation">\</span><span class="token punctuation">;</span><br><br><span class="token builtin class-name">shopt</span> -u extglob</code></pre>
<p>Note that within the script I use an absolute path to point to the cache directory. To test this particular script — since we’re deleting files — you might want to run it first without the command to actually delete:</p>
<pre class="language-shell"><code class="language-shell"><span class="token function">find</span> /home/user123/www/my.site/cache -type f -mtime +5 -name <span class="token string">'*.tmp'</span></code></pre>
<p>You can then call this script in a cron job:</p>
<pre class="language-shell"><code class="language-shell"><span class="token number">0</span> <span class="token number">0</span> * * * /home/user123/www/my.site/scripts/delete_cache.sh <span class="token operator">></span>/home/user123/tmp/cron.delete-cache.log <span class="token operator"><span class="token file-descriptor important">2</span>></span><span class="token file-descriptor important">&amp;1</span></code></pre>
<p>The bit at the beginning is called a ‘cron expression’, and it determines how frequently the job runs. <code>0 0 * * *</code> means ‘run once a day’. Other examples are <code>0 */6 * * *</code> (‘run every six hours’) or <code>*/15 * * * *</code> (‘run every fifteen minutes’). <a href="https://crontab.guru">crontab.guru</a> is a tool that can help with these expressions.</p>
<p>On success the job should write a blank file at <code>cron.trending.delete-cache.log</code>. Any errors will be written to this file, too.</p>
<h2 id="php">PHP</h2>
<p>You can execute PHP scripts, too:</p>
<pre class="language-shell"><code class="language-shell">/usr/local/bin/php /home/user123/my.site/php/my_script.php <span class="token operator">></span>/home/user123/tmp/cron.my_script.dev.log <span class="token operator"><span class="token file-descriptor important">2</span>></span><span class="token file-descriptor important">&amp;1</span></code></pre>
<p>If you’re including other files in the PHP file you’re running, you’ll need to use absolute paths so the files to be included can be found. <code>set_include_path()</code> is useful in this regard:</p>
<pre class="language-php"><code class="language-php"><span class="token function">set_include_path</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'/home/user123/my.site/php/'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">require_once</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'functions.php'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Also specify an absolute path if you’re writing files:</p>
<pre class="language-php"><code class="language-php"><span class="token function">file_put_contents</span><span class="token punctuation">(</span><span class="token string double-quoted-string">"/home/user123/my.site/files/my_file.html"</span><span class="token punctuation">,</span> <span class="token variable">$contents</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Squiz Matrix keywords</title>
      <link href="https://www.jamieonkeys.dev/posts/matrix-keyword-conditionals/"/>
      <updated>2020-08-31T23:00:00+01:00</updated>
      <id>https://www.jamieonkeys.dev/posts/matrix-keyword-conditionals/</id>
      <content type="html">
        <![CDATA[
      <p><a href="https://www.squiz.net/platform/products/cms">Squiz Matrix</a> is a content management system I’ve built stuff with for several years now. I’ve found the keyword replacement and conditional functionality to be consistently useful, so I thought I’d share some tips and tricks thereanent.</p>
<h2 id="what-are-they%3F">What are they?</h2>
<p>Keyword replacements are templating-style keywords used to add dynamic values to pages (or ‘assets’ in Matrix parlance). They start and end with the per cent symbol (e.g. <code>%asset_name%</code>) or — if nested — curly brackets, e.g. <code>{globals_site_name}</code>. The <a href="https://matrix.squiz.net/manuals">Matrix Manuals site</a> has full documentation on <a href="https://matrix.squiz.net/manuals/keyword-replacements/chapters/common-keywords">common</a> and <a href="https://matrix.squiz.net/manuals/keyword-replacements/chapters/global-keywords">global</a> keywords.</p>
<p>Keyword modifiers allow you to modify or perform logic on the values returned by those keywords. They’re denoted by a carat symbol, e.g. <code>^trim</code>. The full <a href="https://matrix.squiz.net/manuals/keyword-replacements/chapters/keyword-modifiers">keyword modifiers</a> documentation is also available on the Squiz site.</p>
<p>Here are a few examples of keyword replacements and conditionals I’ve used in the course of building stuff with Matrix, in order to give you an idea of how they work.</p>
<h2 id="example-1-(decode-json%3B-conditionals%3B-%E2%80%98not-equals%E2%80%99-operator)">Example 1 (decode JSON; conditionals; ‘not equals’ operator)</h2>
<p>This first example could be added to an <code>&lt;a&gt;</code> element. It checks the current page’s lineage in the Matrix <a href="https://matrix.squiz.net/manuals/concepts/chapters/using-the-asset-map">asset map</a> and if it’s not under the site with ID 123456 then it print a <code>target</code> attribute so the link opens in a new window.</p>
<pre class="language-html"><code class="language-html">%asset_linking_lineage^json_decode^index:0^neq:123456:target="_blank"%</code></pre>
<p><code>%asset_linking_lineage%</code> is the keyword and the chain of <code>^</code> modifiers first decodes the JSON in which the keyword returns the lineage, then looks at the first item, checks whether it’s not equal to 123456, then finally prints <code>target=&quot;_blank&quot;</code>. A further <code>:</code> could be added to provide an ‘else’ condition.</p>
<h2 id="example-2-(split-into-array-and-get-index%3B-reverse-array)">Example 2 (split into array and get index; reverse array)</h2>
<p>This next one converts the string value of metadata field <code>foo</code> (<code>item one;item two;item three</code>, for example) into an array (splitting at the semicolon) and gets the first item:</p>
<pre class="language-html"><code class="language-html">%asset_metadata_foo^explode:;^index:0%</code></pre>
<p>And this checks whether the last item in an array (again based on a string delimited by semicolons) is empty or has a value:</p>
<pre class="language-html"><code class="language-html">%asset_metadata_foo^explode:;^array_reverse^index:0^eq::EMPTY:HAS VALUE%</code></pre>
<h2 id="example-3-(nested-keywords%3B-date-based-conditionals%3B-get-asset-attributes%3B-count-words)">Example 3 (nested keywords; date-based conditionals; get asset attributes; count words)</h2>
<p>This is a more complex example, using nested keywords. It could be used in an <a href="https://matrix.squiz.net/manuals/asset-listing">Asset Listing</a> (in the relevant <a href="https://matrix.squiz.net/manuals/asset-listing/chapters/type-formats">Type Format</a> bodycopy asset) to print <a href="https://matrix.squiz.net/manuals/calendar/chapters/single-calendar-event">Single Calendar Events</a> occuring in the past (using <a href="https://www.php.net/manual/en/function.date.php#example-2856">PHP date formatting</a>).</p>
<p>Additionally an elipsis is added if the <code>description</code> attribute is over 40 characters in length.</p>
<pre class="language-html"><code class="language-html">%event_start_date^lt_date:{globals_date_Y-m-d}:<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>event<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>{event_start_datetime_d} {event_start_datetime_F} {event_start_datetime_Y}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h3</span><span class="token punctuation">></span></span>{asset_name_linked}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h3</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>{asset_attribute_description^maxwords:40^trim}{asset_attribute_description^wordcount^gt:40:...:}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{asset_url}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Read More<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>%</code></pre>
<h2 id="example-4-(keywords-in-a-design%3B-as_asset-modifier%3B-trim-output%3B-count-characters%3B-%E2%80%98greater-than%E2%80%99-operator)">Example 4 (keywords in a Design; <code>as_asset</code> modifier; trim output; count characters; ‘greater than’ operator)</h2>
<p>Keywords are also useful in <a href="https://matrix.squiz.net/manuals/designs">Designs</a> (templates). The example below concerns a metadata field of type <a href="https://matrix.squiz.net/manuals/metadata-schemas/chapters/metadata-fields#related-asset-field">Related Asset</a>. Related Asset fields allow you to specify the ID of another asset.</p>
<p>The logic checks first whether there is a value in the Related Asset metadata field <code>foo</code>. If so, it prints a <code>&lt;div&gt;</code> with the related asset’s contents.</p>
<pre class="language-html"><code class="language-html">%begin_asset_metadata_foo%<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>foo<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    %asset_metadata_foo^as_asset:asset_contents_raw%<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>%end_asset%</code></pre>
<p><code>%asset_metadata_foo%</code> is the ID of the related asset specified in the <code>foo</code> metadata field. The <code>^as_asset</code> modfier allows us to access a keyword of that asset, in this case <code>%asset_contents_raw%</code>.</p>
<p>The below example works for a metadata field of type <a href="https://matrix.squiz.net/manuals/metadata-schemas/chapters/metadata-fields#wysiwyg-field">WYSIWYG</a>, where we use <code>^charcount</code> to check whether the field has a value.</p>
<pre class="language-html"><code class="language-html">%begin_asset_metadata_bar^trim^charcount^gt:0%<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bar<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    %asset_metadata_bar%<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>%end_asset%</code></pre>
<p>Here’s another example of the <code>^as_asset</code> modifier used earlier to access the contents of a Related Asset metadata field:</p>
<pre class="language-html"><code class="language-html">%form_submission_id^as_asset:asset_metadata_email%</code></pre>
<p>Here we get the value of the metadata field <code>email</code> on the Form Submission asset with ID <code>form_submission_id</code>. This example could be used in the Email Options screen of a <a href="https://matrix.squiz.net/manuals/custom-form">Custom Form</a>, for example.</p>
<h2 id="example-5-(format-dates%3B-find-and-replace)">Example 5 (format dates; find and replace)</h2>
<p>If you’re dealing with events, the <code>%event_datetime_summary%</code> keyword is useful. You can use modifers to format the output conform to your site’s style guide. The below, for example, will convert <em>30th May 2020 9:00am-5:30pm</em> to <em>30 May 2020 9am – 5.30pm</em>; or <em>30th May 2020 9:00am - 31st May 2020 5:30pm</em> to <em>30 May 2020 9am – 31 May 2020 5.30pm</em>.</p>
<pre class="language-html"><code class="language-html">`%event_datetime_summary^replace:-: – ^replace:th:^replace:rd:^replace:st:^replace:nd:^replace:\:00:^replace:\::.%`</code></pre>
<h2 id="example-6-(get-external-data%3B-php-date-format%3B-mathematical-functions)">Example 6 (get external data; PHP date format; mathematical functions)</h2>
<p>Finally, Matrix allows you to pull in data from <a href="https://matrix.squiz.net/manuals/data">sources outwith the CMS</a>. You can use modifers on keywords representing those external database fields.</p>
<p>The below example pulls a date in Unix time and formats it. It uses the <a href="https://css-tricks.com/snippets/php/php-date-parameters/">PHP Date</a> <code>I</code> parameter to check whether <a href="https://sco.wikipedia.org/wiki/Daylicht_savin_time">daylight savings time (DST)</a> is currently in place. If so (that is to say a value of 1 is returned), 3600 seconds are subtracted and this new value is then formatted into a readable time and printed, otherwise the time will be printed in a readable form as is.</p>
<pre class="language-html"><code class="language-html">%data_source_record_set_time^date_format:I^eq:1:{data_source_record_set_time^subtract:3600^date_format:g\:ia}:{data_source_record_set_time^date_format:g\:ia}%</code></pre>
<hr>
<p>Very occasionally a keyword isn’t documented. One time I wanted to print the submission date of Form Submissions being listed in an Asset Listing but it wasn’t immediately obvious how to do it. Some trial and error yielded <code>%asset_attribute_submitted_short%</code>.</p>
<p>The Paint Layouts <a href="https://matrix.squiz.net/manuals/paint-layouts/chapters/conditional-keywords-screen">Conditional Keywords</a> screen offers some additional conditionals, like checking whether the user has admin access or if Maintenance Mode is on. It also allows you to nest conditionals.</p>
<p>Thanks for reading. For more Matrix-related tips, see my <a href="https://gist.github.com/donbrae/2f6d74a2431c45a885f88eb1da493be5">cheatsheet</a> over at GitHub.</p>

    ]]>
      </content>
    </entry>
  
</feed>
