<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Patrick Galbraith</title>
	<atom:link href="https://www.pjgalbraith.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.pjgalbraith.com</link>
	<description>Web developer - Adelaide, Australia</description>
	<lastBuildDate>Tue, 23 Mar 2021 02:57:48 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=4.1.42</generator>
	<item>
		<title>Uploading files to Salesforce using jsForce</title>
		<link>https://www.pjgalbraith.com/uploading-files-to-salesforce-using-jsforce/</link>
		<comments>https://www.pjgalbraith.com/uploading-files-to-salesforce-using-jsforce/#comments</comments>
		<pubDate>Mon, 14 Sep 2020 07:51:37 +0000</pubDate>
		<dc:creator><![CDATA[Patrick]]></dc:creator>
				<category><![CDATA[Programming]]></category>

		<guid isPermaLink="false">http://www.pjgalbraith.com/?p=1344</guid>
		<description><![CDATA[There aren&#8217;t many examples of how you can upload files (aka ContentVersion entities) to Salesforce using the jsForce library. So I wanted to document what I found while working with the library. Simplest way The easiest way to upload a file is to base64 encode the contents and pass it to the create method. Unfortunately [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>There aren&#8217;t many examples of how you can upload files (aka ContentVersion entities) to Salesforce using the jsForce library. So I wanted to document what I found while working with the library.</p>
<h2>Simplest way</h2>
<p>The easiest way to upload a file is to base64 encode the contents and pass it to the create method. Unfortunately this method is limited to around 35MB.</p>
<pre><code>const uploadContentVersion = (fileName: string, file: Buffer): Promise&lt;jsforce.RecordResult&gt; =&gt;
  connection.sobject('ContentVersion').create({
    PathOnClient : fileName,
    VersionData : file.toString('base64')
  })</code></pre>
<h2>Multipart form data</h2>
<p>The trick here is that you need to encode the request correctly as per the example in the documentation.</p>
<h3>Example request body</h3>
<pre><code>--boundary_string
Content-Disposition: form-data; name="entity_content";
Content-Type: application/json
 
{  
    "Name" : "Marketing Brochure Q1 - Sales",
    "Keywords" : "sales, marketing, first quarter"
}
 
--boundary_string
Content-Type: application/pdf
Content-Disposition: form-data; name="Body"; filename="2011Q1MktgBrochure.pdf"
 
Updated document binary data goes here.
 
--boundary_string--</code></pre>
<h3>Using the nodejs request library</h3>
<p>The popular nodejs `request` library provides a formData option which can be used like below:</p>
<pre><code>import request from 'request'
import jsForce from 'jsforce'

const uploadContentVersion = (metadata: ContentVersionMetadata, file: Buffer): Promise&lt;jsForce.RecordResult&gt; =&gt;
  new Promise((resolve, reject) => {
    const connection: jsForce.Connection = // ...create jsForce connection

    request.post({
      url: connection.instanceUrl + '/services/data/v49.0/sobjects/ContentVersion',
      auth: {
        bearer: connection.accessToken
      },
      formData: {
        entity_content: {
          value: JSON.stringify(metadata),
          options: {
            contentType: 'application/json'
          }
        },
        VersionData: {
          value: file,
          options: {
            filename: metadata.PathOnClient,
            contentType: 'application/octet-stream'
          }
        }
      }
    }, (err, response) =&gt; {
      if (err)
        reject(err)

      resolve(JSON.parse(response.body))
    })
  })
</code></pre>
<h3>Example using `form-data` library</h3>
<p>You could also create the formData object manually like this.</p>
<pre><code>  const formData = new FormData()

  formData.append('entity_content', JSON.stringify(metadata), { contentType: 'application/json' })
  formData.append('VersionData', file, {
    filename: metadata.PathOnClient,
    contentType: 'application/octet-stream'
  })

  // then make request using formData object
</code></pre>
<h2>Full example attaching to Case</h2>
<pre><code>export const addFileToCase = async (caseId: string, fileName: string, file: Buffer): Promise&lt;string&gt; =&gt; {
  // We use the helper method created previously
  const uploadResult = await uploadContentVersion(file, {
    PathOnClient: fileName,

    // For updates you can also add the following params
    // ContentDocumentId: '',
    // ReasonForChange: ''
  })

  if (!uploadResult.success) {
    throw new Error(`Failed to upload file for case &quot;${caseId}&quot;`)
  }

  const contentDocument = await connection.sobject&lt;{
    Id: string,
    ContentDocumentId: string
  }&gt;('ContentVersion').retrieve(uploadResult.id)

  const linkResult = await connection.sobject&lt;TRes&gt;('ContentDocumentLink').create({
    ContentDocumentId: contentDocument.ContentDocumentId,
    LinkedEntityId: caseId,
    ShareType: 'V'
  })

  if (!linkResult.success) {
    throw new Error(`Failed to link content document for case &quot;${caseId}&quot;`)
  }

  return uploadResult.id
})</code></pre>
]]></content:encoded>
			<wfw:commentRss>https://www.pjgalbraith.com/uploading-files-to-salesforce-using-jsforce/feed/</wfw:commentRss>
		<slash:comments>6</slash:comments>
		</item>
		<item>
		<title>Living in a closed source world</title>
		<link>https://www.pjgalbraith.com/living-in-a-closed-source-world/</link>
		<comments>https://www.pjgalbraith.com/living-in-a-closed-source-world/#comments</comments>
		<pubDate>Thu, 05 Apr 2018 13:16:20 +0000</pubDate>
		<dc:creator><![CDATA[Patrick]]></dc:creator>
				<category><![CDATA[Programming]]></category>

		<guid isPermaLink="false">http://www.pjgalbraith.com/?p=1288</guid>
		<description><![CDATA[Recently I was working on a project where I had to work with a C# library with limited to no code level documentation. DnSpy is a debugger and .NET assembly editor. You can use it to edit and debug assemblies even if you don't have any source code available...]]></description>
				<content:encoded><![CDATA[<p>Recently I was working on a project where I had to work with a C# library with limited to no code level documentation. Essentially what I had to work with was something like this:</p>
<pre><code class="csharp">/// &lt;summary&gt;
/// Factory for creating ObjectInfo objects for ObjectInfoProvider.
/// &lt;/summary&gt;
public class ObjectInfoFactory { ... }
</code></pre>
<p>&#8220;<em>Factory for creating ObjectInfo objects for ObjectInfoProvider.</em>&#8221; difficult to say what that means since it could be interpreted different ways. But wouldn&#8217;t it be great if we could see what the code does. Maybe we could even search the codebase and see how it is being used in other parts of the library.</p>
<blockquote><p>Read the source Luke&#8230;</p></blockquote>
<p>In my opinion one of the most beneficial things you can do is read the source code. Documentation is hard and there are many times I have worked with libraries where the documentation and examples are not up-to-date. Not to mention subtle unexpected behaviors that occur when working with other people&#8217;s code.</p>
<p>Reading source code has served me well over the years and as a result I have made a number of humble contributions to open source projects and had fun reading the code for projects like Chromium (Chrome Browser), Samba (SMB/CIFS), FFMPEG, and so on. I find that different ecosystems tend to come with an associated culture. Working more closely with Microsoft .NET recently I have found a bit of a new roadblock&#8230; the source code is not always readily available.</p>
<blockquote><p>
I tried to picture clusters of information as they moved through the computer. What did they look like? Ships, motorcycles. With the circuits like freeways. I kept dreaming of a world I thought I&#8217;d never see. And then one day, I got in.<br />
<small>Kevin Flynn &#8211; Tron Legacy</small></p></blockquote>
<p>DnSpy (<a href="https://github.com/0xd4d/dnSpy">https://github.com/0xd4d/dnSpy</a>) is a debugger and .NET assembly editor. You can use it to edit and debug assemblies even if you don&#8217;t have any source code available. Below I have an example of what it looks like viewing a dll from the DotNetNuke project.</p>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2018/04/dnspy-dnn.jpg" alt="dnspy-dnn" width="1344" height="866" class="aligncenter size-full wp-image-1328" />
<p>That project (DotNetNuke) is open source so we don&#8217;t necessarily need to decompile it but it&#8217;s just an example. This is a tool I have used extensively in the last few months and has made working with closed source legacy code a lot less painful. </p>
<p>It is also possible to use dnSpy to patch dll&#8217;s. There are two main ways to do this you can edit the decompiled C# source code. However doing so will require recompilation and I have found that this seldom works. The other option is to edit the IL code of a method or property. This is a much better way as long as you don&#8217;t mind rolling up your sleeves and reading through the <a href="https://en.wikipedia.org/wiki/List_of_CIL_instructions">CIL instructions</a>. Honestly it&#8217;s not that hard, at least it&#8217;s easier than using a hex editor.</p>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2018/04/dnspy-ilcode.jpg" alt="dnspy-ilcode" width="1403" height="822" class="aligncenter size-full wp-image-1333" />
]]></content:encoded>
			<wfw:commentRss>https://www.pjgalbraith.com/living-in-a-closed-source-world/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Zombicide #2</title>
		<link>https://www.pjgalbraith.com/zombicide-custom-spawn-deck/</link>
		<comments>https://www.pjgalbraith.com/zombicide-custom-spawn-deck/#comments</comments>
		<pubDate>Thu, 01 Jan 2015 13:30:00 +0000</pubDate>
		<dc:creator><![CDATA[Patrick]]></dc:creator>
				<category><![CDATA[Boardgames]]></category>

		<guid isPermaLink="false">http://www.pjgalbraith.com/?p=1026</guid>
		<description><![CDATA[In this follow up post I will look at how I created my custom Zombicide spawn card deck using Photoshop's Variables and Data-sets feature to generate 144 unique cards based on a CSV data source... <a href="/zombicide-custom-spawn-deck/">read more</a>]]></description>
				<content:encoded><![CDATA[<img src="http://www.pjgalbraith.com/wp-content/uploads/2014/10/zombicide-spawn-photo-1.jpg" alt="zombicide-spawn-photo-1" width="1200" height="386" class="aligncenter size-full wp-image-1213" />
<div class="alert">
<h4>TLDR</h4>
<p>Download all the finished files including the cards from Zombicide and all current expansions.<br />
Plus custom spawn cards and photoshop templates.</p>
<p style="margin-top: 20px;"><a href="http://patrickgalbraith.github.io/zombicide-card-database" target="_blank" class="btn btn-small">PREVIEW ONLINE</a> <a href="https://mega.co.nz/#!XBBBnYiA!-Y3gtOaBIlVBktadUi8uZcBE0gCA3sPO08zhd1kWi6A" target="_blank" class="btn btn-small btn-invert">DOWNLOAD FILES</a></p>
<p><a href="/wp-content/uploads/zombicide-spawn-cards_1.0.zip" target="_blank" class="btn btn-small btn-invert">DOWNLOAD FILES (MIRROR)</a></p>
</div>
<div class="alert">
<h4>LOOKING FOR EQUIPMENT CARDS?</h4>
<p>See the original post <a href="/zombicide-custom-equipment-deck/">Zombicide How to print the ultimate custom equipment deck</a>.
</div>
<p>In this follow up post I will look at how I created my custom Zombicide spawn card deck. First of all building the spawn card deck is a little trickier for two reasons. First is that I couldn&#8217;t find a digital source of all the cards such as the companion app that I used for the equipment deck. Secondly because each spawn card is uniquely numbered and there are 144 of them.</p>
<p>Guillotine Games has released high-res spawn card templates. However the cards are essentially blank. Not being one to give up that easily I decided there had to be a way to generate the spawn cards if I had all the card data. Fortunately are some digging online and cross checking with my own sets I managed to put together a spreadsheet containing all the data (this is included in the download).</p>
<h2>Generating the images</h2>
<p>Photoshop has a undocumented feature called Variables &amp; Data Sets. This allows you to define layers as variables of two types. There are text replacement layers which will replace the text in the layer with the text in the data set, and there are visibility layers that will be visible or invisible based on the data. As you can see in the image below I created a spawn card template with all the layer variables defined. Then it was just a matter of writing a simple program to translate the source data found in the spreadsheet into a data set that Photoshop could use which you can see a sample of below.</p>
<pre>number,berserk_bg,berserk_icon,toxic_bg,toxic_icon,blue_text,blue_walker,blue_runner,blue_fatty,blue_abomination,blue_dogz,yellow_text,yellow_walker,yellow_runner,yellow_fatty,yellow_abomination,yellow_dogz,orange_text,orange_walker,orange_runner,orange_fatty,orange_abomination,orange_dogz,red_text,red_walker,red_runner,red_fatty,red_abomination,red_dogz,ft_just_when_you,ft_sewer,ft_cut_them_down,ft_frag_time,ft_bite_me,ft_its_time
1,false,false,false,false,,false,false,false,false,false,2x,false,true,false,false,false,2x,false,true,false,false,false,5x,true,false,false,false,false,false,false,false,false,false,true
2,false,false,false,false,1x,true,false,false,false,false,1x,true,false,false,false,false,6x,true,false,false,false,false,7x,true,false,false,false,false,false,false,false,false,false,true
3,false,false,false,false,1x,true,false,false,false,false,1x,false,true,false,false,false,1x,false,false,true,false,false,4x,false,true,false,false,false,false,false,false,false,false,true
4,false,false,false,false,2x,true,false,false,false,false,3x,true,false,false,false,false,4x,true,false,false,false,false,2x,false,true,false,false,false,false,false,false,false,false,true</pre>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2014/10/zombicide-spawn-photo-2.jpg" alt="zombicide-spawn-photo-2" width="797" height="557" class="aligncenter size-full wp-image-1220" />
<p>With the setup finished all 144 spawn card were generated based on the data set. The only issue was that the card number found in the top right was not on an angle and prefixed with the hash (#) symbol. I fixed that by using a custom Photoshop action which rotated the layer then aligned the hash icon to the left of the layer and then pushed it left a bit. You can see an example of the final cards below.</p>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2014/10/zombicide-spawn-photo-3.jpg" alt="zombicide-spawn-photo-3" width="724" height="615" class="aligncenter size-full wp-image-1222" />
<p>I printed my deck through PrinterStudio which for a 160 card deck came to $10.76. This gave me 16 cards to use for custom cards after re-printing all the base cards. I build some nasty custom cards based on the Hell On Earth set by WakkaWakkatronic <a href="http://boardgamegeek.com/filepage/86115/hell-earth-zombie-spawn-cards">http://boardgamegeek.com/filepage/86115/hell-earth-zombie-spawn-cards</a>. I found these are a great way to mix up the game. How about spawning 15 walkers, or 8 runners at once! I also made some custom Lost Zombivor spawn cards which means I can play with the new Lost Zombivor rules. Both custom sets are included in the download.</p>
<p>Here is the deck type I used <a href="http://www.printerstudio.com/personalized/custom-blank-playing-cards-mini-size.html" target="_blank" rel="nofollow">http://www.printerstudio.com/personalized/custom-blank-playing-cards-mini-size.html</a>. The great thing is that once you have reprinted the base sets it is easy to print additional custom cards at the same size.</p>
<h2>The finished product</h2>
<div class="alert">
<h4>DOWNLOAD</h4>
<p>Download all the finished spawn card files from Zombicide and all current expansions.<br />
Plus custom spawn cards and photoshop templates.</p>
<p style="margin-top: 20px;"><a href="http://patrickgalbraith.github.io/zombicide-card-database" target="_blank" class="btn btn-small">PREVIEW ONLINE</a> <a href="https://mega.co.nz/#!XBBBnYiA!-Y3gtOaBIlVBktadUi8uZcBE0gCA3sPO08zhd1kWi6A" target="_blank" class="btn btn-small btn-invert">DOWNLOAD FILES</a></p>
<p><a href="/wp-content/uploads/zombicide-spawn-cards_1.0.zip" target="_blank" class="btn btn-small btn-invert">DOWNLOAD FILES (MIRROR)</a></p>
</div>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2014/10/zombicide-spawn-photo-4.jpg" alt="zombicide-spawn-photo-4" width="1200" height="672" class="aligncenter size-full wp-image-1224" />
]]></content:encoded>
			<wfw:commentRss>https://www.pjgalbraith.com/zombicide-custom-spawn-deck/feed/</wfw:commentRss>
		<slash:comments>32</slash:comments>
		</item>
		<item>
		<title>Zombicide</title>
		<link>https://www.pjgalbraith.com/zombicide-custom-equipment-deck/</link>
		<comments>https://www.pjgalbraith.com/zombicide-custom-equipment-deck/#comments</comments>
		<pubDate>Sat, 27 Dec 2014 11:09:01 +0000</pubDate>
		<dc:creator><![CDATA[Patrick]]></dc:creator>
				<category><![CDATA[Boardgames]]></category>

		<guid isPermaLink="false">http://www.pjgalbraith.com/?p=1008</guid>
		<description><![CDATA[Lately I've been playing a lot of the awesome hobby that is board games. The last few years have seen a renewed interest in board games likely aided by the internet and its ability to provide crowd funding for a range of new and exciting games. One of these is Zombicide which was funded in 2012 and raised a respectable $780k.

Basically lots of dice rolling, zombie minis, crazy weapons... what more could you ask for in a board game.

How about more crazy weapons and equipment... <a href="/zombicide-custom-equipment-deck">read more</a>]]></description>
				<content:encoded><![CDATA[<img class="aligncenter size-full wp-image-1090" src="http://www.pjgalbraith.com/wp-content/uploads/2014/10/2C7A2734-3.png" alt="2C7A2734-3" width="1130" height="363" />
<div class="alert">
<h4>TLDR</h4>
<p>Download the finished card files including the equipment cards from Zombicide and all current expansions<br />
Plus custom equipment cards and photoshop templates.</p>
<p><strong>Update:</strong> Now contains Season 3, Angry Neighbors, Dog Companions, and Limited Edition/Kickstarter cards.</p>
<p style="margin-top: 20px;"><a href="http://patrickgalbraith.github.io/zombicide-card-database" target="_blank" class="btn btn-small">PREVIEW ONLINE</a> <a href="/wp-content/uploads/zombicide-equipment-card-deck_2.0.zip" target="_blank" class="btn btn-small btn-invert">DOWNLOAD FILES (v2.0)</a></p>
</div>
<div class="alert">
<h4>LOOKING FOR SPAWN CARDS?</h4>
<p>See my follow up post <a href="/zombicide-custom-spawn-deck/">Zombicide #2 How to print the ultimate spawn card deck</a>.
</div>
<p>Lately I&#8217;ve been playing a lot of the awesome hobby that is board games. The last few years have seen a renewed interest in board games likely aided by the internet and its ability to provide crowd funding for a range of new and exciting games. One of these is Zombicide which was funded in 2012 and raised a respectable $780k.</p>
<blockquote><p><i>Zombicide</i> is a thematic co-op game for 1 to 6 players. Each player controls between one (for 6 players) and four (solo game) survivors, humans trapped in a zombie-infested town.</p></blockquote>
<p>Basically lots of dice rolling, zombie minis, crazy weapons&#8230; what more could you ask for in a board game.</p>
<p>&#8230;</p>
<p>How about more crazy weapons and equipment!</p>
<p>With that thought began my quest for the ultimate Zombicide equipment deck.</p>
<h2>Sourcing the images</h2>
<p>There are a number of custom card printers but none that I found can print the mini-card size that Zombicide uses so the only alternative is to re-print the entire base deck in a slightly different size. Which sounds like a bad idea if it wasn&#8217;t for the fact that you can easily and cheaply print cards through a number of online printers such as PrinterStudio and ArtsCow but more about that later.</p>
<p>Guillotine Games has released high-res basic card templates and some of the cards in high-res on their website. However not all of the cards are available online in digital format. After a whole lot of searching online I managed to find that there is a Zombicide companion app.</p>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2014/10/zombicide-app-2.jpg" alt="zombicide-app" width="1130" class="aligncenter size-full wp-image-1172" />
<p>After extracting all of the equipment card image assets from the app. I now had all 52 unique equipment cards from Season 1, 2 &amp; Toxic City Mall. Time to make some custom cards.</p>
<h2>Creating custom cards</h2>
<p>Unfortunately I wasn&#8217;t able to find a equipment card photoshop template that contains all the features I needed so I build a new one which I have included in the download. Using this template I created a bunch of custom equipment cards. Here is a sample of some of the custom cards mixed in with the base cards.</p>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2014/10/zombicide-equip-photo-2.jpg" alt="zombicide-custom-cards" width="1130" class="aligncenter size-full wp-image-1181" />
<p>For reference the fonts used are:</p>
<ul>
<li>Crackhouse</li>
<li>ITC Avant Garde Gothic Std &#8211; Bold Condensed</li>
<li>Helvetica Neue LT Std &#8211; Extra Black Condensed</li>
<li>Eurostile Bold Condensed</li>
</ul>
<h2>Printing the deck</h2>
<p>In order to re-print the equipment decks you will need to print the following.</p>
<p><strong>Edit:</strong> See this document for updated list of equipment:<br />
<a href="https://docs.google.com/spreadsheets/d/1XLGrwFbj3wk0c37scQS-GoIr4dh_YDeJpOjYveWdH74/edit#gid=1143787238" target="_blank">https://docs.google.com/spreadsheets/d/1XLGrwFbj3wk0c37scQS-GoIr4dh_YDeJpOjYveWdH74/edit#gid=1143787238</a></p>
<p>That works out to 173 equipment cards and 46 wound cards. However you won&#8217;t need 46 wound cards so I recommend you print around 16-24 wound cards. I printed my deck through PrinterStudio which for a 224 card deck came to $14.60 (<strong>Note:</strong> these prices have now changed, you may need to shop around or wait for a sale). This gave me 27 cards to use for custom cards after re-printing all the base cards. Here is the deck type I used <a href="http://www.printerstudio.com/personalized/custom-blank-playing-cards-mini-size.html" target="_blank" rel="nofollow">http://www.printerstudio.com/personalized/custom-blank-playing-cards-mini-size.html</a>.</p>
<h2>The finished product</h2>
<div class="alert">
<h4>DOWNLOAD</h4>
<p>Download all the finished files including the equipment cards from Zombicide and all current expansions<br />
Plus custom equipment cards and photoshop templates.</p>
<p><strong>Update:</strong> Now contains Season 3, Angry Neighbors, Dog Companions, and Limited Edition/Kickstarter cards.</p>
<p style="margin-top: 20px;"><a href="http://patrickgalbraith.github.io/zombicide-card-database" target="_blank" class="btn btn-small">PREVIEW ONLINE</a> <a href="/wp-content/uploads/zombicide-equipment-card-deck_2.0.zip" target="_blank" class="btn btn-small btn-invert">DOWNLOAD FILES (v2.0)</a></p>
</div>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2014/10/zombicide-equip-photo-1.jpg" alt="zombicide-equip-photo-1" width="1200" height="800" class="aligncenter size-full wp-image-1190" />
<div class="alert">
<h4>LOOKING FOR SPAWN CARDS?</h4>
<p>See my follow up post <a href="/zombicide-custom-spawn-deck/">Zombicide #2 How to print the ultimate spawn card deck</a>.
</div>
]]></content:encoded>
			<wfw:commentRss>https://www.pjgalbraith.com/zombicide-custom-equipment-deck/feed/</wfw:commentRss>
		<slash:comments>62</slash:comments>
		</item>
		<item>
		<title>IE Conditional Compilation and @TODO</title>
		<link>https://www.pjgalbraith.com/ie-conditional-compilation-and-todo/</link>
		<comments>https://www.pjgalbraith.com/ie-conditional-compilation-and-todo/#comments</comments>
		<pubDate>Wed, 29 Oct 2014 21:46:57 +0000</pubDate>
		<dc:creator><![CDATA[Patrick]]></dc:creator>
				<category><![CDATA[Programming]]></category>

		<guid isPermaLink="false">http://www.pjgalbraith.com/?p=1047</guid>
		<description><![CDATA[ ]]></description>
				<content:encoded><![CDATA[<p>So today I was working on fixing compatibility issues in Internet Explorer on a new project when I ran into an error like this on IE versions 8 to 10.</p>
<pre><code>SCRIPT1004: Expected ';'
File: example.js, Line: 9, Column: 35</pre>
<p></code></p>
<p>And the line it was pointing to looked like this.</p>
<pre><code>var test = 1; //@todo: add new feature</pre>
<p></code></p>
<p>Well it turns out that if conditional compilation (<a href="http://msdn.microsoft.com/en-us/library/ie/121hztk3(v=vs.94).aspx">http://msdn.microsoft.com/en-us/library/ie/121hztk3(v=vs.94).aspx</a>) is turned on using <code>/*@cc_on @*/</code> in any script. Using the @ symbol anywhere at the start of a comment causes Internet Explorer to throw a syntax error.</p>
<p>The minimum reproducable test case is:</p>
<pre><code>/*@cc_on @*/
var test = 1; //@todo: add new feature</pre>
<p></code></p>
<p>Which can be fixed by putting a space in front of the @ symbol like this.</p>
<pre><code>/*@cc_on @*/
var test = 1; // @todo: add new feature</pre>
<p></code></p>
<p>This becomes an issue in larger codebases where you cannot know if other scripts have turned conditional compilation on at all. As far as I know there isn't a workaround. However you can show a console message warning that conditional compilation is on like this.</p>
<pre><code>/*@if (@_jscript) if ('console' in self &#038;&#038; 'log' in console) console.log('Conditional compilation is turned on!'); @end @*/</pre>
<p></code></p>
<p>Keep in mind however that a syntax error caused by the @ symbol will trigger before the above runs.</p>
]]></content:encoded>
			<wfw:commentRss>https://www.pjgalbraith.com/ie-conditional-compilation-and-todo/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Simple Template Engine</title>
		<link>https://www.pjgalbraith.com/simple-template-engine/</link>
		<comments>https://www.pjgalbraith.com/simple-template-engine/#comments</comments>
		<pubDate>Sun, 26 Oct 2014 11:22:04 +0000</pubDate>
		<dc:creator><![CDATA[Patrick]]></dc:creator>
				<category><![CDATA[Programming]]></category>

		<guid isPermaLink="false">http://www.pjgalbraith.com/?p=1015</guid>
		<description><![CDATA[There are a number of different JavaScript template engines. However in this post I just wanted to explore how you would go about evolving from a simplistic approach into something more complex. Faux-Mustache So the simplest approach to converting the template and data into the output we need is to just use String.prototype.replace(). Note I [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>There are a number of different JavaScript template engines. However in this post I just wanted to explore how you would go about evolving from a simplistic approach into something more complex.</p>
<h2>Faux-Mustache</h2>
<p>So the simplest approach to converting the template and data into the output we need is to just use <code>String.prototype.replace()</code>. Note I am using mustache/handlebars syntax for the template syntax in this section. None of the template variables escape html so don&#8217;t use in production with unsafe data.</p>
<pre><code>var tpl    = "&lt;p&gt;Is this a template? {{test}}.&lt;/p&gt;";
var output = tpl.replace('{{test}}', 'Yes it is!');
</code></pre>
<p>However this will only work if we have a single <code>{{test}}</code> template variable in our template because <code>String.prototype.replace()</code> only replaces the first item. You can use the third flags parameter to do a global replace using a string input but it is non-standard. So we have to use a regex instead.</p>
<pre><code>var tpl    = "&lt;p&gt;Is this a template? {{test}}.&lt;/p&gt;";
var re     = /\{\{test\}\}/gmi;
var output = tpl.replace(re, 'Yes it is!');
</code></pre>
<p>This works and for really simple applications you could easily just use this method instead of a template engine. But the data that we want to replace is hard-coded which means we can&#8217;t update it very easily and we need a new replace call for each additional variable. So what we want is to be able to pass in any data as properties of an object like this.</p>
<pre><code>var Template = function(tpl, data){
    return tpl.replace(/\{\{(.*?)\}\}/gmi, function(match, p1) {
        var prop = p1.trim();
        return data.hasOwnProperty(prop) ? data[prop] : '';
    });
};</code></pre>
<pre><code>var tpl    = "&lt;p&gt;Is this a template? {{test}} {{test2}}.&lt;/p&gt;";
var data   = {
    test: 'Yes',
    test2: 'it is!'
};
var output = Template(tpl, data);</code></pre>
<p>This is better but it still has some issues. Most notably we have no way to access descendant properties. It would be great to be able to do this <code>{{user.name}}</code>. So lets make it happen.</p>
<pre><code>var Template = function(tpl, data){
    return tpl.replace(/\{\{(.*?)\}\}/gmi, function(match, p1) {
        var prop = p1.trim().split('.'),
            obj  = data;
        while(prop.length &#038;&#038; typeof(obj) != 'undefined')
            obj = obj[prop.shift()]
        return typeof(obj) != 'undefined' ? obj : '';
    });
};</code></pre>
<pre><code>var tpl    = "&lt;p&gt;Hi {{user.name}}! You are from {{user.location}}.&lt;/p&gt;";
var data   = {
    user: {
        name: 'Pat',
        location: 'Australia'
    }
};
var output = Template(tpl, data);</code></pre>
<p>Great so that works pretty well and it&#8217;s only nine lines of code to boot. However there are still two big features missing and they are template compilation and the ability to work with arrays. </p>
<h2>Compilation</h2>
<p>Template compilation allows us to avoid re-executing the regex and looping through every property every time we use the template. So lets look at how we could update the previous example to return a compiled function.</p>
<pre><code>var Template = function(tpl){
    var cursor = 0,
        code = "var r=[];\n", 
        re = /\{\{(.*?)\}\}/gmi;
    
    var add = function(line, tag) {
        if(tag) {
            code += "r.push(data."+line+");\n";
        } else {
            code += line != '' ? 'r.push("' + line.replace(/"/g, '"') + "\");\n" : '';
        }
    };
    
    while(match = re.exec(tpl)) {
        add(tpl.slice(cursor, match.index));
        add(match[1], true);
        cursor = match.index + match[0].length;
    }
    
    add(tpl.substr(cursor, tpl.length - cursor));
    code += 'return r.join("");';

    try {
        result = new Function('data', code); 
    } catch(err) {
        console.error("'" + err.message + "'", " in \n\nCode:\n", code, "\n");
    }

    return result;
};</code></pre>
<pre><code>var tpl    = "&lt;p&gt;Hi {{user.name}}! You are from {{user.location}}.&lt;/p&gt;";
var data   = {
    user: {
        name: 'Pat',
        location: 'Australia'
    }
};
var compiledTemplate = Template(tpl);
var output = compiledTemplate(data);</code></pre>
<p>So now we have a function we can call to parse the template with different data whenever we need to. Note the above is based on the blog post by John Resig (jQuery) <a href="http://ejohn.org/blog/javascript-micro-templating/">here</a> and the template engine from Krasimir Tsonev <a href="http://krasimirtsonev.com/blog/article/Javascript-template-engine-in-just-20-line">here</a>.</p>
<p>How it works is that it generates a <code>new Function()</code> that looks like this:</p>
<pre><code>function(data) {
    var r=[];
        
    r.push(&quot;&lt;p&gt;Hi &quot;);
    r.push(data.user.name);
    r.push(&quot;! You are from &quot;);
    r.push(data.user.location);
    r.push(&quot;.&lt;/p&gt;&quot;);

    return r.join(&quot;&quot;); 
}</code></pre>
<p>Lets update it to work with arrays. Ideally it would be great to be able to use a template like this <code>{{#items}}{{.}}{{/items}}</code> which loops through an array of items just like you would when using mustache.</p>
<pre><code>var Template = function(tpl, data){
    var cursor = 0,
        code = &quot;var r=[];\n&quot;, 
        re = /\{\{(.*?)\}\}/gmi;
    var add = function(line, tag) {
        if(tag) {
            if(line.charAt(0) === '#') {
                code += &quot;for(var i=0, len=data.&quot;+line.slice(1)+&quot;.length; i&lt;len; i++){\n&quot;;
                code += &quot;var el=data.&quot;+line.slice(1)+&quot;[i];\n&quot;;
            } else if (line.charAt(0) === '/') {
                code += &quot;};\n&quot;;
            } else {
                code += line === '.' ? &quot;r.push(el || data);\n&quot; : &quot;r.push(data.&quot;+line+&quot;);\n&quot;;
            }
        } else {
            code += line != '' ? 'r.push(&quot;' + line.replace(/&quot;/g, '&quot;') + &quot;\&quot;);\n&quot; : '';
        }
    };
  
    while(match = re.exec(tpl)) {
        add(tpl.slice(cursor, match.index));
        add(match[1], true);
        cursor = match.index + match[0].length;
    }
    
    add(tpl.substr(cursor, tpl.length - cursor));
    code += 'return r.join(&quot;&quot;);';
    try { result = new Function('data', code); }
    catch(err) { console.error(&quot;'&quot; + err.message + &quot;'&quot;, &quot; in \n\nCode:\n&quot;, code, &quot;\n&quot;); }
    return result;
};</code></pre>
<pre><code>var tpl    = &quot;&lt;p&gt;Hi {{user.name}}! You like:&lt;/p&gt;&quot;+
             &quot;&lt;ul&gt;{{#user.likes}}&lt;li&gt;{{.}}&lt;/li&gt;{{/user.likes}}&lt;/ul&gt;&quot;;
var data   = {
    user: {
        name: 'Pat',
        likes: ['Code', 'Boardgames', 'Cooking']
    }
};
var output = Template(tpl)(data);</code></pre>
<p>So as you can see this is a little more complex. Of course it is still missing a lot of features but the point isn&#8217;t to create a fully featured template engine (plenty of those exist). </p>
<p>Unfortunately it isn&#8217;t possible to re-create all of the mustache features using this approach simply because of the ambiguity in the way mustache handles objects, arrays, and variables with the hash (#) prefix. The alternative is to use something like this <a href="http://jsfiddle.net/a9o9Lw0x/1/">http://jsfiddle.net/a9o9Lw0x/1/</a> however this will likely perform a lot worse since we need to dynamically handle the block (#) tags and at that point you may as well use a proper template engine.</p>
<h2>Embedded Javascript (EJS)</h2>
<p>Now that we have template compilation it is fairly trivial to make a Embedded Javascript (EJS) style template engine. Let&#8217;s have a look at how it is done.</p>
<pre><code>var Template = function(tpl){
    var cursor = 0,
        code   = &quot;with(data) { var r=[];\n&quot;, 
        re     = /&lt;%(.*?)%&gt;/gmi;
    
    var add = function(line, tag) {
        if(tag) {
            code += line.charAt(0) === '=' ? &quot;r.push(&quot;+line.slice(1).trim()+&quot;);\n&quot; : line+&quot;\n&quot;;
        } else {
            code += line != '' ? 'r.push(&quot;' + line.replace(/&quot;/g, '&quot;') + &quot;\&quot;);\n&quot; : '';
        }
    };
    
    while(match = re.exec(tpl)) {
        add(tpl.slice(cursor, match.index));
        add(match[1], true);
        cursor = match.index + match[0].length;
    }
    
    add(tpl.substr(cursor, tpl.length - cursor));
    code += 'return r.join(&quot;&quot;); }';
    try { result = new Function('data', code); }
    catch(err) { console.error(&quot;'&quot; + err.message + &quot;'&quot;, &quot; in \n\nCode:\n&quot;, code, &quot;\n&quot;); }
    return result;
};</code></pre>
<pre><code>var tpl    = &quot;&lt;p&gt;Hi &lt;%= user.name %&gt;! You are from &lt;%= user.location %&gt;.&lt;/p&gt;&quot;+
             &quot;&lt;ul&gt;&quot;+
             &quot;&lt;% for(var i=0; i &lt; user.likes.length; i++) { %&gt;&quot;+
                 &quot;&lt;li&gt;&lt;%= user.likes[i] %&gt;&lt;/li&gt;&quot;+
             &quot;&lt;% } %&gt;&quot;+
             &quot;&lt;/ul&gt;&quot;;
var data   = {
    user: {
        name: 'Pat',
        location: 'Australia',
        likes: ['Code', 'Boardgames', 'Cooking']
    }
};
var output = Template(tpl)(data);</code></pre>
<p>Well that&#8217;s it for the rundown on how to create a simple template engine. </p>
]]></content:encoded>
			<wfw:commentRss>https://www.pjgalbraith.com/simple-template-engine/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>PHP Foreach Reference Gotcha</title>
		<link>https://www.pjgalbraith.com/php-foreach-reference-gotcha/</link>
		<comments>https://www.pjgalbraith.com/php-foreach-reference-gotcha/#comments</comments>
		<pubDate>Sat, 25 Oct 2014 12:27:03 +0000</pubDate>
		<dc:creator><![CDATA[Patrick]]></dc:creator>
				<category><![CDATA[Miscellaneous]]></category>

		<guid isPermaLink="false">http://www.pjgalbraith.com/?p=1036</guid>
		<description><![CDATA[ ]]></description>
				<content:encoded><![CDATA[<pre><code>$items = array(1, 2, 3);

foreach($items as &#038;$item) {
    echo $item;
}

// prints: 123

echo $item;

// prints: 3

foreach($items as $item) {
    echo $item;
}

// prints: 122
</code></pre>
<p>What?<br />
This happens because after the first loop <code>$item</code> maintains the reference to <code>3</code>.</p>
<p>Of course PHP does not have block scoping so we then assign that reference to the current loop item in the second loop overwriting the initial value.</p>
<p>The fix. Always unset the item after the closing brace.</p>
<pre><code>foreach($items as &#038;$item) {
    echo $item;
} unset($item);
</code></pre>
<p>Or put it in a self-invoking function.</p>
<pre><code>call_user_func(function() use (&#038;$items) {
    foreach($items as &#038;$item) {
        echo $item;
    }
});</code></pre>
]]></content:encoded>
			<wfw:commentRss>https://www.pjgalbraith.com/php-foreach-reference-gotcha/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>Drawing Animated Curves</title>
		<link>https://www.pjgalbraith.com/drawing-animated-curves-javascript/</link>
		<comments>https://www.pjgalbraith.com/drawing-animated-curves-javascript/#comments</comments>
		<pubDate>Sun, 27 Oct 2013 11:23:19 +0000</pubDate>
		<dc:creator><![CDATA[Patrick]]></dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[as3]]></category>
		<category><![CDATA[flash]]></category>
		<category><![CDATA[tweenmax]]></category>

		<guid isPermaLink="false">http://www.pjgalbraith.com/?p=391</guid>
		<description><![CDATA[ After digging through some older projects today I came across this code from FollowThemAround.com used to animate the flight path from the Radiohead tour map. It was originally written in AS3 but I ported it to JS for fun. ]]></description>
				<content:encoded><![CDATA[<p>After digging through some older projects today I came across this code from FollowThemAround.com used to animate the flight path from the Radiohead tour map. It was originally written in AS3 but I ported it to JS for fun. </p>
<p>Basically drawBezierSplit() allow you to draw a section of a quadratic bezier curve. It uses a quadratic bezier curve (quadraticCurveTo) because this is the default curve type built into the AS3 drawing API. For reference the HTML5 canvas API uses cubic curves by default.</p>
<pre><code>/**
 * Animates bezier-curve
 * 
 * @param ctx       The canvas context to draw to
 * @param x0        The x-coord of the start point
 * @param y0        The y-coord of the start point
 * @param x1        The x-coord of the control point
 * @param y1        The y-coord of the control point
 * @param x2        The x-coord of the end point
 * @param y2        The y-coord of the end point
 * @param duration  The duration in milliseconds
 */
function animatePathDrawing(ctx, x0, y0, x1, y1, x2, y2, duration) {
    var start = null;
    
    var step = function animatePathDrawingStep(timestamp) {
        if (start === null)
            start = timestamp;
        
        var delta = timestamp - start,
            progress = Math.min(delta / duration, 1);
        
        // Clear canvas
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
        
        // Draw curve
        drawBezierSplit(ctx, x0, y0, x1, y1, x2, y2, 0, progress);
        
        if (progress < 1) {
            window.requestAnimationFrame(step);
        }
    };
    
    window.requestAnimationFrame(step);
}

/**
 * Draws a splitted bezier-curve
 * 
 * @param ctx       The canvas context to draw to
 * @param x0        The x-coord of the start point
 * @param y0        The y-coord of the start point
 * @param x1        The x-coord of the control point
 * @param y1        The y-coord of the control point
 * @param x2        The x-coord of the end point
 * @param y2        The y-coord of the end point
 * @param t0        The start ratio of the splitted bezier from 0.0 to 1.0
 * @param t1        The start ratio of the splitted bezier from 0.0 to 1.0
 */
function drawBezierSplit(ctx, x0, y0, x1, y1, x2, y2, t0, t1) {
    ctx.beginPath();
    
    if( 0.0 == t0 &#038;&#038; t1 == 1.0 ) {
        ctx.moveTo( x0, y0 );
        ctx.quadraticCurveTo( x1, y1, x2, y2 );
    } else if( t0 != t1 ) {
        var t00 = t0 * t0,
            t01 = 1.0 - t0,
            t02 = t01 * t01,
            t03 = 2.0 * t0 * t01;
        
        var nx0 = t02 * x0 + t03 * x1 + t00 * x2,
            ny0 = t02 * y0 + t03 * y1 + t00 * y2;
        
        t00 = t1 * t1;
        t01 = 1.0 - t1;
        t02 = t01 * t01;
        t03 = 2.0 * t1 * t01;
        
        var nx2 = t02 * x0 + t03 * x1 + t00 * x2,
            ny2 = t02 * y0 + t03 * y1 + t00 * y2;
        
        var nx1 = lerp ( lerp ( x0 , x1 , t0 ) , lerp ( x1 , x2 , t0 ) , t1 ),
            ny1 = lerp ( lerp ( y0 , y1 , t0 ) , lerp ( y1 , y2 , t0 ) , t1 );
        
        ctx.moveTo( nx0, ny0 );
        ctx.quadraticCurveTo( nx1, ny1, nx2, ny2 );
    }
    
    ctx.stroke();
    ctx.closePath();
}

/**
 * Linearly interpolate between two numbers v0, v1 by t
 */
function lerp(v0, v1, t) {
    return ( 1.0 - t ) * v0 + t * v1;
}

document.addEventListener('DOMContentLoaded',function(){
    var docCanvas = document.getElementById('canvas'),
        ctx = docCanvas.getContext('2d');

    animatePathDrawing(ctx, 0, 100, 150, -50, 300, 100, 5000);
});
</code></pre>
<p><iframe width="100%" height="300" src="http://jsfiddle.net/a595enjw/1/embedded/result,js,html" allowfullscreen="allowfullscreen" frameborder="0"></iframe></p>
<p>Note: Click on the result tab to restart the animation.</p>
<p>Credit to Andre Michelle (andre-michelle.com) for the original inspiration.</p>
]]></content:encoded>
			<wfw:commentRss>https://www.pjgalbraith.com/drawing-animated-curves-javascript/feed/</wfw:commentRss>
		<slash:comments>12</slash:comments>
		</item>
		<item>
		<title>RAGEagain</title>
		<link>https://www.pjgalbraith.com/rageagain/</link>
		<comments>https://www.pjgalbraith.com/rageagain/#comments</comments>
		<pubDate>Wed, 29 Aug 2012 04:21:38 +0000</pubDate>
		<dc:creator><![CDATA[Patrick]]></dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Web Design]]></category>

		<guid isPermaLink="false">http://www.pjgalbraith.com/?p=783</guid>
		<description><![CDATA[Rage is a popular all-night Australian music video program broadcast on ABC1 on Friday nights, Saturday mornings and Saturday nights. It was first screened on the weekend of Friday, 17 April 1987.

Since 1998 rage has posted all their episode playlists online. So I created a website that combines these playlists with YouTube allowing you to travel back through time and re-experience rage's recent history.]]></description>
				<content:encoded><![CDATA[<a href="http://www.rageagain.com" target="_blank"><img src="http://www.pjgalbraith.com/wp-content/uploads/2012/08/rageagain-1024x579.jpg" alt="rageagain.com" title="rageagain" width="1024" height="579" class="alignnone size-large wp-image-790" /></a>
<div style="font-size:18px; text-align:center"><a href="http://www.rageagain.com" target="_blank">www.rageagain.com</a></div>
<p>&nbsp;</p>
<p>Rage is a popular all-night Australian music video program broadcast on ABC1 on Friday nights, Saturday mornings and Saturday nights. It was first screened on the weekend of Friday, 17 April 1987.</p>
<p>Since 1998 rage has posted all their episode playlists online. So I created a website that combines these playlists with YouTube allowing you to travel back through time and re-experience rage&#8217;s recent history.</p>
<p><em>Please note: all the matching of track to video is done programmatically so some tracks particularly older ones may not be available (depending on whether someone has uploaded them).</em></p>
<h3>Behind the Scenes (v2 &#8211; 2020)</h3>
<p>The original version of the site was written using the Lithium PHP Framework in 2012. The site ran well until 2019 when a number of issues cropped up. So I had a plan in mind for a re-envisioned version to address the following issues:</p>
<ul>
<li>Over the years Google has been reducing the number of requests that can be made to the Youtube search API. When the site was first developed the project had millions of requests in quota. Now it is down to 100s per month. I tried contacting the black hole that is Google support with no luck. So I needed an alternative to using the Youtube search API.</li>
<li>The ABC website scraper needs to be rewritten since the ABC site has changed.</li>
<li>I wanted to make the website static, and have it connect to a serverless backend to avoid having to host and maintain a server.</li>
</ul>
<p>Given these requirements I decided on the following plan:</p>
<ol>
<li>Write a new API backend hosted on Cloudflare workers.</li>
<li>Dump the playlist data stored in MySQL into flat files (JSON).</li>
<li>Use scheduled Github workers to scrape the ABC website and update the data directory.</li>
<li>Convert the frontend to be a static single page application (SPA).</li>
</ol>
<p>The project is broken into three parts; the API backend, the static frontend SPA, and the ABC scraper script run via Github workers.</p>
<p>For the backend API my initial plan was to write this using Rust (https://www.rust-lang.org/). Cloudflare workers have good support for Rust and I enjoy working with the language.</p>
<p>However I ran into some issues writing a YouTube search scraper and given that there was already a NodeJS package for this purpose it didn&#8217;t seem like a great idea to re-invent the wheel. So I ended up writing the final version using Typescript. </p>
<p>The logic is fairly simple when a request is made for a specific music video, it will first check for a cached result stored in Cloudflare KV storage. If the cached data cannot be found it will then go off and grab the data from YouTube.</p>
<p>There is also another endpoint that proxies the data from Github, providing the correct CORS headers for the frontend.</p>
<p>All in all it was fun to re-visit the project.</p>
<p>Checkout the interview with ABC about the site for more details <a href="https://web.archive.org/web/20150402201331/https://open.abc.net.au/explore/31540">https://web.archive.org/web/20150402201331/https://open.abc.net.au/explore/31540</a></p>
<h3>Source code and data</h3>
<p>Scraping the data (particularly the 90s web pages) wasn&#8217;t easy due to broken and inconsistent markup so I have posted a database dump in case anyone else wants to play with the data.</p>
<p>
<strong>Playlists:</strong> 1,694<br />
<strong>Tracks:</strong> 201,316<br />
<strong>Approx. Play Time:</strong> 1 year, 173 days, 5 hours, 46 mins
</p>
<p><a href='https://github.com/patrickgalbraith/rageagain'>https://github.com/patrickgalbraith/rageagain</a> (Source code and data repo)</p>
<h2>Changelog</h2>
<h3>17th January 2020</h3>
<p><strong>Features</strong></p>
<ul>
<li>RAGEAgain is back after a long hiatus!</li>
<li>Fixing the site required a complete rewrite. Source code is now available here <a href="https://github.com/patrickgalbraith/rageagain">https://github.com/patrickgalbraith/rageagain</a>.</li>
</ul>
<h3>12th June 2017</h3>
<p><strong>Features</strong></p>
<ul>
<li>Adjusted Youtube search algorithm to ignore Aria chart information and de-rank reaction and karaoke videos</li>
</ul>
<h3>4th March 2013</h3>
<p><strong>Features</strong></p>
<ul>
<li>Migrated to new sever (initial load should be significantly faster)</li>
<li>Finally implemented listing by special</li>
<li>Updated to jQuery 1.9.1</li>
</ul>
<h3>3rd September 2012</h3>
<p><strong>Features</strong></p>
<ul>
<li>Updated to include latest playlists</li>
<li>New special playlist; Top 200 which plays the most played tracks in ascending order (i.e. most played first).</li>
<li>Individual playlist/tracks are now bookmarkable and browser back button works</li>
<li>Keyboard shortcuts (spacebar = pause/play, arrow left = prev, arrow right = next)</li>
<ul>
<li><strong>Note:</strong> If you click on the video you may need to click outside of video for the keyboard events to trigger again. This is because the video will capture any keyboard events.</li>
</ul>
<li>Added anchor links to quickly skip to specific year.</li>
<li>Added warning for users on unsupported/outdated browsers.</li>
<li>Updated jquery to 1.8.1 (mainly fixes player not appearing in Internet Explorer 8/9)</li>
</ul>
<p><strong>Bug Fixes</strong></p>
<ul>
<li>Fixed issue with tracklist being capped at 200 which caused the first tracks to not be played.</li>
<li>Massive database overhaul</li>
<ul>
<li>Fixed broken 1999 playlists</li>
<li>Added 30+ missing/incomplete playlists</li>
<li>Removed a few duplicate playlists</li>
<li>Removed all duplicate tracks (some tracklists had been entered twice)</li>
<li>Fixed tracks containing unescaped html entites and tags</li>
</ul>
</ul>
<h3>29th August 2012</h3>
<p>First public release</p>
<h3>28th July 2012</h3>
<p>Private release</p>
]]></content:encoded>
			<wfw:commentRss>https://www.pjgalbraith.com/rageagain/feed/</wfw:commentRss>
		<slash:comments>90</slash:comments>
		</item>
		<item>
		<title>Bookmarklet &#8211; Force Youtube HTML5 Embed</title>
		<link>https://www.pjgalbraith.com/bookmarklet-force-youtube-html5-embed/</link>
		<comments>https://www.pjgalbraith.com/bookmarklet-force-youtube-html5-embed/#comments</comments>
		<pubDate>Tue, 08 May 2012 03:07:22 +0000</pubDate>
		<dc:creator><![CDATA[Patrick]]></dc:creator>
				<category><![CDATA[Programming]]></category>

		<guid isPermaLink="false">http://www.pjgalbraith.com/?p=756</guid>
		<description><![CDATA[Here is a simple JavaScript bookmarklet that replaces YouTube videos embedded using the old flash object embed method with the new iframe embed method. What this means is that you can force a lot of embeded YouTube videos to play with the HTML5 video player. This is particularly useful on IOS devices (iPhone, iPad) with [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>Here is a simple JavaScript bookmarklet that replaces YouTube videos embedded using the old flash object embed method with the new iframe embed method. What this means is that you can force a lot of embeded YouTube videos to play with the HTML5 video player. This is particularly useful on IOS devices (iPhone, iPad) with the YouTube app disabled.<br />
<span id="more-756"></span></p>
<h3>Installation (iPad)</h3>
<ol style="margin-left:20px">
<li>Add this page as a bookmark</li>
<li>Select and copy the Javascript in the textarea below.</li>
<li>Go to your bookmarks and tap &#8220;edit&#8221;. Then tap the new bookmark you just made of this page.</li>
<li>Edit the name and paste the Javascript in the field for the URL.</li>
<li>Click done.</li>
</ol>
<p><textarea style="height: 259px; width: 514px;">javascript:(function(){var%20objectTags=document.getElementsByTagName(&#8216;object&#8217;);var%20objectTagsArr=new%20Array();for(var%20i=0;i<objectTags.length;i++){objectTagsArr[i]=objectTags[i];}for(i=0;i<objectTagsArr.length;i++){var%20objectTag=objectTagsArr[i];if(objectTag.data.indexOf('http://www.youtube.com/')>=0){var%20youtubeID=objectTag.data.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&#038;]v=)|youtu\.be\/)([^&#8217;&#038;?\/ ]{11})/i)[1];var%20newIframeElem=document.createElement(&#8216;iframe&#8217;);newIframeElem.class=&#8217;youtube-player';newIframeElem.type=&#8217;text/html';newIframeElem.width=640;newIframeElem.height=385;newIframeElem.src=&#8217;http://www.youtube.com/embed/&#8217;+youtubeID;newIframeElem.frameborder=0;var%20newElem=objectTag.parentNode.replaceChild(newIframeElem,objectTag);}}})();</textarea></p>
<h3>Source code with formatting</h3>
<pre><code>javascript:(function(){
	var objectTags = document.getElementsByTagName('object');
	var objectTagsArr = new Array();
	
	for (var i = 0; i < objectTags.length; i++) {
		objectTagsArr[i] = objectTags[i];
	}
	
	for (i = 0; i < objectTagsArr.length; i++) {
		var objectTag = objectTagsArr[i];
		if(objectTag.data.indexOf('http://www.youtube.com/') >= 0) {
			var youtubeID = objectTag.data.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&#038;]v=)|youtu\.be\/)([^'&#038;?\/ ]{11})/i)[1];
			
			var newIframeElem = document.createElement('iframe');
			newIframeElem.class = 'youtube-player';
			newIframeElem.type = 'text/html';
			newIframeElem.width = 640;
			newIframeElem.height = 385;
			newIframeElem.src = 'http://www.youtube.com/embed/'+youtubeID;
			newIframeElem.frameborder = 0;
			
			objectTag.parentNode.replaceChild(newIframeElem, objectTag);
		}
	}
})();</code></pre>
]]></content:encoded>
			<wfw:commentRss>https://www.pjgalbraith.com/bookmarklet-force-youtube-html5-embed/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
