<?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 &#187; Programming</title>
	<atom:link href="https://www.pjgalbraith.com/category/programming/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>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>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>
		<item>
		<title>Default Thumbnail Plus</title>
		<link>https://www.pjgalbraith.com/default-thumbnail-plus/</link>
		<comments>https://www.pjgalbraith.com/default-thumbnail-plus/#comments</comments>
		<pubDate>Wed, 07 Dec 2011 12:55:50 +0000</pubDate>
		<dc:creator><![CDATA[Patrick]]></dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[wordpress]]></category>

		<guid isPermaLink="false">http://www.pjgalbraith.com/?p=490</guid>
		<description><![CDATA[Automatically display a default Post Thumbnail image, either the first image attached to a post, or set default images for specific categories, tags, or custom taxonomies. Supports custom fields to ease upgrading. Download: http://wordpress.org/extend/plugins/default-thumbnail-plus &#160; &#160; Thumbnail Selection Hierarchy The hierarchy below determines what thumbnail will be chosen, in other words the plugin first checks [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>Automatically display a default Post Thumbnail image, either the first image attached to a post, or set default images for specific categories, tags, or custom taxonomies. Supports custom fields to ease upgrading.</p>
<p><span style="font-size: 120%;">Download: <a href="http://wordpress.org/extend/plugins/default-thumbnail-plus">http://wordpress.org/extend/plugins/default-thumbnail-plus</a></span></p>
<form action="https://www.paypal.com/cgi-bin/webscr" method="post"><input type="hidden" name="cmd" value="_s-xclick" /><br />
<input type="hidden" name="encrypted" value="-----BEGIN PKCS7-----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-----END PKCS7----- " /><br />
<input type="image" name="submit" src="https://www.paypalobjects.com/en_AU/i/btn/btn_donateCC_LG.gif" alt="PayPal — The safer, easier way to pay online." /><br />
<img src="https://www.paypalobjects.com/en_AU/i/scr/pixel.gif" alt="" width="1" height="1" border="0" /></form>
<p>&nbsp;<br />
&nbsp;<br />
<a href="http://www.pjgalbraith.com/wp-content/uploads/2011/12/screenshot-1.jpg"><img src="http://www.pjgalbraith.com/wp-content/uploads/2011/12/screenshot-1.jpg" alt="" title="dtp-1" width="652" height="859" class="alignleft size-full wp-image-533" style="border-bottom:1px solid #CCC; border-top:1px solid #CCC" /></a><br />
<span id="more-490"></span></p>
<h3>Thumbnail Selection Hierarchy</h3>
<p>The hierarchy below determines what thumbnail will be chosen, in other words the plugin first checks if the user has manually set a featured image. If not then it checks if it has a custom field containing an attachment id, and so on. Once the plugin gets to the last step if no default thumbnail image is set then no post thumbnail will be displayed.</p>
<ol>
<li>Featured Image</li>
<li>Custom Field</li>
<li>Image Attachment</li>
<li>Embedded image</li>
<li>Embedded video</li>
<li>Category/Tag/Taxonomy Thumbnail</li>
<li>Default Thumbnail</li>
</ol>
<h3>Add Filter (Category/Tag/Taxonomy Thumbnail)</h3>
<p>You can add filters which allow you to select a thumbnail for a specific category, tag, or custom taxonomy. The value field is required and must contain either a comma separated list of slug, id, or title for a existing category, tag, or custom taxonomy. Any post that does not have a post-thumbnail set and has the specified tag, or is a member of the category or taxonomy will show the default thumbnail specified.</p>
<h3>Use image attachment if available</h3>
<p>Automatically use the post&#8217;s first available image attachment for the thumbnail. This is useful for older posts that haven&#8217;t got a featured image set.</p>
<h3>Custom Field</h3>
<p>Enter a custom field key here, it&#8217;s value if set will become the default post thumbnail for that post. The custom field value can either be an Attachment ID, or a link to an image.</p>
<p><em>Note: If the custom field value contains a link to an image it will output a simple image tag and attempt to populate the width and height attributes. However the aspect ratio may not match other thumbnails. The plugin does not check if the image exists or is of valid type or size.<br />
</em></p>
<h3>Use embedded image if available</h3>
<p>Automatically use the post&#8217;s first available embedded image for the thumbnail. This is useful if you embed external images.</p>
<p><em>Note: This option does not work with has_post_thumbnail() or get_post_thumbnail_id(). If the &#8220;cache external images&#8221; option is set then the plugin will automatically resize, crop the image and store a local copy.</em></p>
<h3>Use embedded video if available</h3>
<p>Automatically use the post&#8217;s first available embedded video for the thumbnail (YouTube only).</p>
<p><em>Note: This option does not work with has_post_thumbnail() or get_post_thumbnail_id(). If the &#8220;cache external images&#8221; option is set then the plugin will automatically resize, crop the image and store a local copy.</em></p>
<h3>Excluded posts</h3>
<p>List of posts to be ignored by this plugin. Comma separated e.g. 10, 2, 7, 14.</p>
<p><em>Note: These posts may still display a thumbnail if one has been set manually (using the &#8220;Set featured image&#8221; button on the post page).</em></p>
<h3>Functions</h3>
<p>The following functions work to directly return post thumbnail even if the plugin hooks have been disabled. This allows you to disable the filters and call the plugin manually. This should only be used if you are creating a custom theme and can ensure that DefaultThumbnailPlus is installed.</p>
<p><strong>dpt_post_thumbnail_id($post_id, $size = null, $attr = &#8221;)</strong><br />
<em>Returns the post thumbnail ID. Doesn&#8217;t work with embedded images.</em></p>
<p><strong>dpt_post_thumbnail_html($post_id, $size = null, $attr = &#8221;)</strong><br />
<em>Directly calls the default post thumbnail function. Works the same as calling get_the_post_thumbnail().</em></p>
<p><strong>dpt_post_thumbnail_src($post_id, $size = null)</strong><br />
<em>Returns href to post thumbnail image.</em></p>
<hr />
<h3>Why can&#8217;t I see any thumbnails with my custom theme?</h3>
<p>A number of themes use <code>has_post_thumbnail()</code> to check if a post thumbnail exists before calling <code>the_post_thumbnail()</code> to display the thumbnail. The problem with this is that thumbnails that are linked to directly (either external images or video thumbnails) won&#8217;t return correctly for this check and therefore may not show.<br />
&nbsp;<br />
To fix this issue you will need to look for the following code in your theme:</p>
<pre><code class="php">if(has_post_thumbnail()) {
    the_post_thumbnail(...);
}
</code></pre>
<p>&nbsp;<br />
And simply remove the call to <code>has_post_thumbnail()</code> so you are left with this:</p>
<pre><code class="php">the_post_thumbnail(...);</code></pre>
<h4>TwentyEleven</h4>
<p>The header only works with image attachments, i.e. embedded images/video thumbnails won&#8217;t show. </p>
<p>The header attachment width also has to be greater than the custom-header width option (default 1000px). This is generally a good thing though since it prevents low resolution images from displaying in the header.</p>
<h4>WooThemes</h4>
<p>WooThemes call a function called <code>woo_image()</code> rather than using the default WordPress functionality. This causes the default post thumbnail to never show.</p>
<p>So to fix this you need to override the <code>woo_image()</code> function with a custom implementation that reverts to using WordPress&#8217;s in-built functions. To do this paste the following code into the <strong>TOP</strong> of the themes <code>functions.php</code> file found in <code>/wp-content/themes/your_themes_name</code> after the opening <code>&lt;?php</code> tag.</p>
<pre><code class="php">
//Override the default woothemes image function and revert to using the default wordpress function.
if ( ! is_admin() &#038;&#038; ! function_exists('woo_image')){
    function woo_image($args) {
        global $post;

        $size = null;
        $attr = array();
        $before = '';
        $after = '';
        $class = '';

        if ( !is_array($args) )
            parse_str( $args, $args );

        extract($args);

        if(empty($id))
            $id = $post->ID;

        if(isset($width) &#038;&#038; isset($height))
            $size = array($width, $height);

        $attr['class'] = 'woo-image '.$class;

        if(isset($alt))
            $attr['alt'] = $alt;

        $output = $before . get_the_post_thumbnail($id, $size, $attr) . $after;

        if($return == TRUE)
            return $output;
        else
            echo $output;
    }
}
</code></pre>
<p>&nbsp;<br />
<em>The downside is that it won&#8217;t recreate all of woo_image()&#8217;s features. However it shouldn&#8217;t cause issues with most themes.</em></p>
]]></content:encoded>
			<wfw:commentRss>https://www.pjgalbraith.com/default-thumbnail-plus/feed/</wfw:commentRss>
		<slash:comments>120</slash:comments>
		</item>
		<item>
		<title>Truncating Text &amp; HTML with PHP</title>
		<link>https://www.pjgalbraith.com/truncating-text-html-with-php/</link>
		<comments>https://www.pjgalbraith.com/truncating-text-html-with-php/#comments</comments>
		<pubDate>Thu, 24 Nov 2011 15:06:18 +0000</pubDate>
		<dc:creator><![CDATA[Patrick]]></dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[php]]></category>

		<guid isPermaLink="false">http://www.pjgalbraith.com/?p=394</guid>
		<description><![CDATA[A common occurrence in programming are problems that seem simple on the surface but become increasingly complicated the longer you look at them. While working on the latest version of the Recent Posts Plus WordPress plugin I stumbled upon a variety of issues with text/html truncation which I hope to outline in this post. So [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>A common occurrence in programming are problems that seem simple on the surface but become increasingly complicated the longer you look at them. While working on the latest version of the <a href="http://www.pjgalbraith.com/2011/08/recent-posts-plus/">Recent Posts Plus</a> WordPress plugin I stumbled upon a variety of issues with text/html truncation which I hope to outline in this post. So lets start with text truncation&#8230;<br />
&nbsp;</p>
<h2>Truncating Text</h2>
<p>There are two ways in which I needed to truncate text; by the number of characters, and by the number of words.</p>
<p>Original text:</p>
<p style="padding-left: 30px;"><span style="color: #0000ff; font-family: monospace">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque tellus enim, iaculis eget imperdiet et, sodales id magna.</span></p>
<p>Limited to 9 chars:</p>
<p style="padding-left: 30px; font-family: monospace"><span style="color: #0000ff;">Lorem ips<br /></span>123456789</p>
<p>Limited to 9 words:</p>
<p style="padding-left: 30px; font-family: monospace">
    <span style="color: #0000ff;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque</span><br />
      <span style="color: #000000;">1     2     3    4   5         6           7      8       9</span>
</p>
<p>&nbsp;</p>
<h3>Truncating Text by Char</h3>
<p><span id="more-394"></span><br />
The function to truncate text by char is very simple.</p>
<pre><code class="php">function truncate_chars($text, $limit, $ellipsis = '...') {
    if( strlen($text) &gt; $limit ) 
        $text = trim(substr($text, 0, $limit)) . $ellipsis; 
    return $text;
}</code></pre>
<p>&nbsp;<br />
Basically you just count the number of chars using <a href="http://php.net/manual/en/function.strlen.php" target="_blank">strlen</a> if the length is greater than the limit then return a subset of the string.<br />
&nbsp;<br />
You can improve this function by preventing it from chopping a word in half by searching the string for the next white space character.</p>
<pre><code class="php">function truncate_chars($text, $limit, $ellipsis = '...') {
    if( strlen($text) &gt; $limit ) {
        $endpos = strpos(str_replace(array("\r\n", "\r", "\n", "\t"), ' ', $text), ' ', $limit);
        if($endpos !== FALSE)
            $text = trim(substr($text, 0, $endpos)) . $ellipsis;
    }
    return $text;
}</code></pre>
<p>&nbsp;<br />
<em>Just one little note about these functions they aren&#8217;t multibyte safe. If you need to perform truncation of multibyte strings (Korean, Chinese, Japanese, etc) then look into using the multibyte string functions instead <a href="http://php.net/ref.mbstring" target="_blank">http://php.net/ref.mbstring</a>.</em><br />
&nbsp;</p>
<h3>Truncating Text by Word</h3>
<p>Truncating text by words is fairly simple as well we just break the text into pieces using whitespace characters as the separator. However I have come across a few potential pitfalls. The obvious way to break the text into words is like this <code>$words = explode(' ', $text, $limit + 1);</code>. However the issue with this is that it doesn&#8217;t take into account all whitespace characters (like tabs and line breaks).</p>
<p>The way that WordPress and a lot of other people do it is like this:</p>
<pre><code class="php">function truncate_words($text, $limit, $ellipsis = '...') {
    $words = preg_split("/[\n\r\t ]+/", $text, $limit + 1, PREG_SPLIT_NO_EMPTY);
    if ( count($words) > $limit ) {
        array_pop($words);
        $text = implode(' ', $words);
        $text = $text . $excerpt_more;
    }
    return $text;
}</code></pre>
<p>&nbsp;</p>
<p>Which works well however it has one major issue in that if you feed it text containing line breaks or tabs it will replace those line breaks with normal spaces &#8216; &#8216;. The better way to do it is rather than using implode on the word array, capture the offset of the last word using the PREG_SPLIT_OFFSET_CAPTURE flag then use this offset to split the string.</p>
<pre><code class="php">function truncate_words($text, $limit, $ellipsis = '...') {
    $words = preg_split("/[\n\r\t ]+/", $text, $limit + 1, PREG_SPLIT_NO_EMPTY|PREG_SPLIT_OFFSET_CAPTURE);
    if (count($words) > $limit) {
        end($words); //ignore last element since it contains the rest of the string
        $last_word = prev($words);
           
        $text =  substr($text, 0, $last_word[1] + strlen($last_word[0])) . $ellipsis;
    }
    return $text;
}</code></pre>
<p>&nbsp;<br />
This method ensures that the integrity of the whitespace is retained. It is also significantly faster (see performance comparison at the end of the post).<br />
&nbsp;</p>
<h3>Truncating with CSS</h3>
<p>One interesting thing about all of this is that in many cases text truncation should really be handled at the client side. It makes sense that the page contains the full-text data but trims it if the content area is too small to hold everything when displayed. </p>
<p>A good example to illustrate this is imagine if you design a menu which contains a list of items. Now the width of the menu is fixed so some items may need to be truncated to fit the width of the list. However if the user can adjust the font size suddenly the list needs to be remade server-side to compensate for presentation changes. You can also run into issues if the font is not monospaced (monospaced = each character has same width) since limiting the number of characters won&#8217;t result in a consistent width. </p>
<p>In this case truncation can be handled with the CSS <code>text-overflow</code> property.<br />
&nbsp;<br />
<img src="http://www.pjgalbraith.com/wp-content/uploads/2011/11/text-overflow.png" alt="Image Credit - developer.mozilla.org" /><br />
&nbsp;<br />
As the below code shows it is very easy to implement and is fairly well supported (IE6, Opera 9+, Safari, Chrome)</p>
<pre><code class="css">p {
  white-space: nowrap;
  width: 250px;
  overflow: hidden;
  -o-text-overflow: ellipsis;
  -ms-text-overflow: ellipsis;
  text-overflow: ellipsis;
}</code></pre>
<p>&nbsp;<br />
See <a href="http://developer.mozilla.org/en/CSS/text-overflow" target="_blank">http://developer.mozilla.org/en/CSS/text-overflow</a> for more info.</p>
<p>However this doesn&#8217;t work at all if you apply it to a DIV which contains nested elements. The nested elements will be clipped without an ellipsis being appended. You can workaround this issue by applying the same styles to all the nested elements.</p>
<pre><code class="css">#mydiv, #mydiv * {
  white-space: nowrap;
  width: 250px;
  overflow: hidden;
  -o-text-overflow: ellipsis;
  -ms-text-overflow: ellipsis;
  text-overflow: ellipsis;
}</code></pre>
<p>&nbsp;</p>
<h2>Truncating HTML</h2>
<p>Ok so now we get to the tricky part. The issue with truncating HTML is that you cannot use an approach similar to what you would use for plain text. The reason for this is obvious if you truncate HTML as text then you run the risk of splitting the text in the middle of a HTML tag or attribute. So what we need is to be able to differentiate between the HTML tags and text, this requires parsing the HTML.</p>
<p>If you look online you will find some solutions to this problem that use regular expressions to parse the HTML. Parsing HTML with regular expressions <a href="http://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454#1732454" target="_blank">is a bad idea</a> if you are interested in how HTML is parsed by browsers then have a look at <a href="http://www.html5rocks.com/en/tutorials/internals/howbrowserswork" target="_blank">How Browsers Work: Behind the Scenes of Modern Web Browsers</a>. </p>
<p>Fortunately the PHP <a href="http://php.net/manual/en/class.domdocument.php" target="_blank">DomDocument</a> class can handle all the difficult parsing for us. So what we want is to parse the HTML using <a href="http://php.net/manual/en/class.domdocument.php" target="_blank">DomDocument</a> that way we can accurately traverse the DOM and count the number of characters or words in any of the text nodes. Then we can remove any superfluous nodes and reconstruct the HTML document.</p>
<pre><code class="php">class TruncateHTML {
    
    public static function truncateChars($html, $limit, $ellipsis = '...') {
        
        if($limit <= 0 || $limit >= strlen(strip_tags($html)))
            return $html;
        
        $dom = new DOMDocument();
        $dom->loadHTML($html);
        
        $body = $dom->getElementsByTagName("body")->item(0);
        
        $it = new DOMLettersIterator($body);
        
        foreach($it as $letter) {
            if($it->key() >= $limit) {
                $currentText = $it->currentTextPosition();
                $currentText[0]->nodeValue = substr($currentText[0]->nodeValue, 0, $currentText[1] + 1);
                self::removeProceedingNodes($currentText[0], $body);
                self::insertEllipsis($currentText[0], $ellipsis);
                break;
            }
        }
        
        return preg_replace('~<(?:!DOCTYPE|/?(?:html|head|body))[^>]*>\s*~i', '', $dom->saveHTML());
    }
    
    public static function truncateWords($html, $limit, $ellipsis = '...') {
        
        if($limit <= 0 || $limit >= self::countWords(strip_tags($html)))
            return $html;
        
        $dom = new DOMDocument();
        $dom->loadHTML($html);
        
        $body = $dom->getElementsByTagName("body")->item(0);
        
        $it = new DOMWordsIterator($body);
        
        foreach($it as $word) {            
            if($it->key() >= $limit) {
                $currentWordPosition = $it->currentWordPosition();
                $curNode = $currentWordPosition[0];
                $offset = $currentWordPosition[1];
                $words = $currentWordPosition[2];
                
                $curNode->nodeValue = substr($curNode->nodeValue, 0, $words[$offset][1] + strlen($words[$offset][0]));
                
                self::removeProceedingNodes($curNode, $body);
                self::insertEllipsis($curNode, $ellipsis);
                break;
            }
        }
        
        return preg_replace('~<(?:!DOCTYPE|/?(?:html|head|body))[^>]*>\s*~i', '', $dom->saveHTML());
    }
    
    private static function removeProceedingNodes(DOMNode $domNode, DOMNode $topNode) {        
        $nextNode = $domNode->nextSibling;
        
        if($nextNode !== NULL) {
            self::removeProceedingNodes($nextNode, $topNode);
            $domNode->parentNode->removeChild($nextNode);
        } else {
            //scan upwards till we find a sibling
            $curNode = $domNode->parentNode;
            while($curNode !== $topNode) {
                if($curNode->nextSibling !== NULL) {
                    $curNode = $curNode->nextSibling;
                    self::removeProceedingNodes($curNode, $topNode);
                    $curNode->parentNode->removeChild($curNode);
                    break;
                }
                $curNode = $curNode->parentNode;
            }
        }
    }
    
    private static function insertEllipsis(DOMNode $domNode, $ellipsis) {    
        $avoid = array('a', 'strong', 'em', 'h1', 'h2', 'h3', 'h4', 'h5'); //html tags to avoid appending the ellipsis to
        
        if( in_array($domNode->parentNode->nodeName, $avoid) &#038;&#038; $domNode->parentNode->parentNode !== NULL) {
            // Append as text node to parent instead
            $textNode = new DOMText($ellipsis);
            
            if($domNode->parentNode->parentNode->nextSibling)
                $domNode->parentNode->parentNode->insertBefore($textNode, $domNode->parentNode->parentNode->nextSibling);
            else
                $domNode->parentNode->parentNode->appendChild($textNode);
        } else {
            // Append to current node
            $domNode->nodeValue = rtrim($domNode->nodeValue).$ellipsis;
        }
    }
    
    private static function countWords($text) {
        $words = preg_split("/[\n\r\t ]+/", $text, -1, PREG_SPLIT_NO_EMPTY);
        return count($words);
    }
    
}</code></pre>
<p><a href="http://www.pjgalbraith.com/wp-content/uploads/TruncateHTML.php.txt" target="_blank">View the above source code standalone</a><br />
<a href="http://www.pjgalbraith.com/wp-content/uploads/TruncateHTML.v1.php.txt" target="_blank">View old source code standalone (version 1)</a><br />
&nbsp;</p>
<pre><code class="php">/**
 * Iterates individual words of DOM text and CDATA nodes
 * while keeping track of their position in the document.
 *
 * Example:
 *
 *  $doc = new DOMDocument();
 *  $doc->load('example.xml');
 *  foreach(new DOMWordsIterator($doc) as $word) echo $word;
 *
 * @author pjgalbraith http://www.pjgalbraith.com
 * @author porneL http://pornel.net (based on DOMLettersIterator available at http://pornel.net/source/domlettersiterator.php)
 * @license Public Domain
 *
 */

final class DOMWordsIterator implements Iterator {
    
    private $start, $current;
    private $offset, $key, $words;

    /**
     * expects DOMElement or DOMDocument (see DOMDocument::load and DOMDocument::loadHTML)
     */
    function __construct(DOMNode $el)
    {
        if ($el instanceof DOMDocument) $this->start = $el->documentElement;
        else if ($el instanceof DOMElement) $this->start = $el;
        else throw new InvalidArgumentException("Invalid arguments, expected DOMElement or DOMDocument");
    }
    
    /**
     * Returns position in text as DOMText node and character offset.
     * (it's NOT a byte offset, you must use mb_substr() or similar to use this offset properly).
     * node may be NULL if iterator has finished.
     *
     * @return array
     */
    function currentWordPosition()
    {
        return array($this->current, $this->offset, $this->words);
    }

    /**
     * Returns DOMElement that is currently being iterated or NULL if iterator has finished.
     *
     * @return DOMElement
     */
    function currentElement()
    {
        return $this->current ? $this->current->parentNode : NULL;
    }
    
    // Implementation of Iterator interface
    function key()
    {
        return $this->key;
    }
    
    function next()
    {
        if (!$this->current) return;

        if ($this->current->nodeType == XML_TEXT_NODE || $this->current->nodeType == XML_CDATA_SECTION_NODE)
        {
            if ($this->offset == -1)
            {
                $this->words = preg_split("/[\n\r\t ]+/", $this->current->textContent, -1, PREG_SPLIT_NO_EMPTY|PREG_SPLIT_OFFSET_CAPTURE);
            }
            $this->offset++;
            
            if ($this->offset < count($this->words)) { 
                $this->key++;
                return;
            }
            $this->offset = -1;
        }

        while($this->current->nodeType == XML_ELEMENT_NODE &#038;&#038; $this->current->firstChild)
        {
            $this->current = $this->current->firstChild;
            if ($this->current->nodeType == XML_TEXT_NODE || $this->current->nodeType == XML_CDATA_SECTION_NODE) return $this->next();
        }

        while(!$this->current->nextSibling &#038;&#038; $this->current->parentNode)
        {
            $this->current = $this->current->parentNode;
            if ($this->current === $this->start) {$this->current = NULL; return;}
        }

        $this->current = $this->current->nextSibling;

        return $this->next();
    }

    function current()
    {
        if ($this->current) return $this->words[$this->offset][0];
        return NULL;
    }

    function valid()
    {
        return !!$this->current;
    }

    function rewind()
    {
        $this->offset = -1; $this->words = array();
        $this->current = $this->start;
        $this->next();
    }
}</code></pre>
<p><a href="http://www.pjgalbraith.com/wp-content/uploads/DOMWordsIterator.php.txt" target="_blank">View the above source code standalone</a><br />
&nbsp;</p>
<pre><code class="php">/**
 * Iterates individual characters (Unicode codepoints) of DOM text and CDATA nodes
 * while keeping track of their position in the document.
 *
 * Example:
 *
 *  $doc = new DOMDocument();
 *  $doc->load('example.xml');
 *  foreach(new DOMLettersIterator($doc) as $letter) echo $letter;
 *
 * NB: If you only need characters without their position
 *     in the document, use DOMNode->textContent instead.
 *
 * @author porneL http://pornel.net
 * @license Public Domain
 *
 */
final class DOMLettersIterator implements Iterator
{
    private $start, $current;
    private $offset, $key, $letters;

    /**
     * expects DOMElement or DOMDocument (see DOMDocument::load and DOMDocument::loadHTML)
     */
    function __construct(DOMNode $el)
    {
        if ($el instanceof DOMDocument) $this->start = $el->documentElement;
        else if ($el instanceof DOMElement) $this->start = $el;
        else throw new InvalidArgumentException("Invalid arguments, expected DOMElement or DOMDocument");
    }

    /**
     * Returns position in text as DOMText node and character offset.
     * (it's NOT a byte offset, you must use mb_substr() or similar to use this offset properly).
     * node may be NULL if iterator has finished.
     *
     * @return array
     */
    function currentTextPosition()
    {
        return array($this->current, $this->offset);
    }

    /**
     * Returns DOMElement that is currently being iterated or NULL if iterator has finished.
     *
     * @return DOMElement
     */
    function currentElement()
    {
        return $this->current ? $this->current->parentNode : NULL;
    }

    // Implementation of Iterator interface
    function key()
    {
        return $this->key;
    }

    function next()
    {
        if (!$this->current) return;

        if ($this->current->nodeType == XML_TEXT_NODE || $this->current->nodeType == XML_CDATA_SECTION_NODE)
        {
            if ($this->offset == -1)
            {
                // fastest way to get individual Unicode chars and does not require mb_* functions
                preg_match_all('/./us',$this->current->textContent,$m); $this->letters = $m[0];
            }
            $this->offset++; $this->key++;
            if ($this->offset < count($this->letters)) return;
            $this->offset = -1;
        }

        while($this->current->nodeType == XML_ELEMENT_NODE &#038;&#038; $this->current->firstChild)
        {
            $this->current = $this->current->firstChild;
            if ($this->current->nodeType == XML_TEXT_NODE || $this->current->nodeType == XML_CDATA_SECTION_NODE) return $this->next();
        }

        while(!$this->current->nextSibling &#038;&#038; $this->current->parentNode)
        {
            $this->current = $this->current->parentNode;
            if ($this->current === $this->start) {$this->current = NULL; return;}
        }

        $this->current = $this->current->nextSibling;

        return $this->next();
    }

    function current()
    {
        if ($this->current) return $this->letters[$this->offset];
        return NULL;
    }

    function valid()
    {
        return !!$this->current;
    }

    function rewind()
    {
        $this->offset = -1; $this->letters = array();
        $this->current = $this->start;
        $this->next();
    }
}</code></pre>
<p><a href="http://pornel.net/source/domlettersiterator.php" target="_blank">View the above source code standalone</a> (Note the DOMLettersIterator class was created by <a href="http://pornel.net">pornel</a>)</p>
<p>Well I didn&#8217;t say it was going to be easy. As you can see it takes a fair bit of work to do and a lot more lines of code compared to the simple text truncate functions. It is also significantly more expensive to process.</p>
<p>You can use the above class like this:</p>
<pre><code class="php">require('DOMLettersIterator.php'); 
require('DOMWordsIterator.php'); 
require('TruncateHTML.php');

$html = &#039;&lt;p&gt;This is &lt;strong&gt;test&lt;/strong&gt; html text.&lt;/p&gt;&#039;;

$output = TruncateHTML::truncateChars($html, &#039;11&#039;, &#039;...&#039;);
echo $output;

$output = TruncateHTML::truncateWords($html, &#039;3&#039;, &#039;...&#039;);
echo $output;</code></pre>
<p>&nbsp;<br />
In summary if you need to truncate HTML I would always recommend you first consider whether it is possible to strip the tags and process it as plain text. Of course you could always throw caution to the wind and devise a rudimentary regex based solution, which may work ok if you can live with the limitations. Otherwise the above approach (or something similar to it) is a good option especially if accuracy is important.<br />
&nbsp;</p>
<h2>Performance Comparison</h2>
<p>The following numbers represent the average execution time in microseconds to execute each function 1000 times. Smaller is better.</p>
<p>truncate_chars: 1,446<br />
truncate_chars2: 4,858 (second version that retains words)<br />
truncate_words: 23,401 (WordPress version)<br />
truncate_words2: 12,562<br />
TruncateHTML::truncateChars: 874,875<br />
TruncateHTML::truncateWords: 1,397,687</p>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2011/11/Untitled.jpg" style="border: 0 none">
<p>As you can see the HTML truncation functions are over 100 times slower than their text equivalents so you definitely want to consider caching their output.</p>
]]></content:encoded>
			<wfw:commentRss>https://www.pjgalbraith.com/truncating-text-html-with-php/feed/</wfw:commentRss>
		<slash:comments>31</slash:comments>
		</item>
		<item>
		<title>Recent Posts Plus (WordPress Widget)</title>
		<link>https://www.pjgalbraith.com/recent-posts-plus/</link>
		<comments>https://www.pjgalbraith.com/recent-posts-plus/#comments</comments>
		<pubDate>Sat, 13 Aug 2011 10:15:01 +0000</pubDate>
		<dc:creator><![CDATA[Patrick]]></dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[wordpress]]></category>

		<guid isPermaLink="false">http://www.pjgalbraith.com/?p=253</guid>
		<description><![CDATA[An advanced version of the WordPress Recent Posts widget, allows display of thumbnails, post excerpt, author, comment count, and more. Provides options to trim the title and override the post order and type. Also includes a simple template parser so you can override the default output. Download: http://wordpress.org/extend/plugins/recent-posts-plus 1: Title Default: Recent Posts This field [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>An advanced version of the WordPress Recent Posts widget, allows display of thumbnails, post excerpt, author, comment count, and more. Provides options to trim the title and override the post order and type. Also includes a simple template parser so you can override the default output.</p>
<p><span style="font-size: 120%;">Download: <a href="http://wordpress.org/extend/plugins/recent-posts-plus">http://wordpress.org/extend/plugins/recent-posts-plus</a></span></p>
<p><span id="more-253"></span></p>
<form action="https://www.paypal.com/cgi-bin/webscr" method="post"><input type="hidden" name="cmd" value="_s-xclick" /><br />
<input type="hidden" name="encrypted" value="-----BEGIN PKCS7-----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-----END PKCS7----- " /><br />
<input type="image" name="submit" src="https://www.paypalobjects.com/en_AU/i/btn/btn_donateCC_LG.gif" alt="PayPal — The safer, easier way to pay online." /><br />
<img src="https://www.paypalobjects.com/en_AU/i/scr/pixel.gif" alt="" width="1" height="1" border="0" /></form>
<img class="size-full wp-image-264" title="screenshot-num" src="http://www.pjgalbraith.com/wp-content/uploads/2011/08/screenshot-num.jpg" alt="" width="266" height="696" />
<h3>1: Title</h3>
<p><em>Default: Recent Posts</em></p>
<p>This field sets the title of the widget. It works the same as every other widget.</p>
<h3>2: Number of posts</h3>
<p><em>Default: 5</em></p>
<p>Changes how many posts are displayed in the widget. Enter &#8216;-1&#8242; to display all posts.</p>
<h3>3: Include post thumbnail</h3>
<p><em>Default: no</em></p>
<p>Displays the post thumbnails of each post. </p>
<p><em>If this option is hidden you may not have a theme with post-thumbnails enabled. You can manually enable and add post-thumbnails to each post or you can download the <a href="http://www.pjgalbraith.com/2011/12/default-thumbnail-plus/">Default Thumbnail Plus</a> plugin automatically display a default image.</em></p>
<p style="text-align: center;"><img class="size-full wp-image-271 aligncenter" title="rpp-step3" src="http://www.pjgalbraith.com/wp-content/uploads/2011/08/rpp-step3.jpg" alt="" width="228" height="148" style="border: 1px solid #CCC" /></p>
<h3>4: Include post excerpt</h3>
<p><em>Default: no</em></p>
<p>Displays the post excerpt of each post.</p>
<h3>5: Show expert options</h3>
<p><em>Default: no</em></p>
<p>Show/hide the expert options panel. These options should be used with more care.</p>
<h3>6: Limit post title</h3>
<p><em>Default: <strong>(no limit)</strong></em></p>
<p>Limits the numbers of characters or words to display of each posts title.<br />
<strong>Example: </strong> For the post title &#8220;Look at this title&#8221; setting this option to 7 chars would result in &#8220;Look at&#8230;&#8221;</p>
<h3>7: Limit post excerpt</h3>
<p><em>Default: <strong>(no limit)</strong></em></p>
<p>Limits the numbers of characters or words to display of each posts excerpt.</p>
<h3>8: Limit ellipsis</h3>
<p><em>Default: &#8230;</em></p>
<p>Sets the string to append to post titles or excerpts that have been shortened.</p>
<h3>9: Post date format</h3>
<p><em>Default: M j</em></p>
<p>Sets the PHP date format to use when displaying the post date.<br />
See <a href="http://php.net/manual/en/function.date.php" target="_blank">http://php.net/manual/en/function.date.php</a> for overview of the options available.</p>
<p><em>Note: the date format can be overriden in the output template, which allows for multiple date formats in one entry, see section &#8220;12: Widget output template&#8221;.</em></p>
<h3>10: Thumbnail size</h3>
<p><em>Default: 50 x 50</em></p>
<p>Sets the size of the post thumbnail in pixels.</p>
<h3>11: WP_Query options</h3>
<p>This is an advanced option that modifies what posts are shown in the widget. This option specifies extra arguments to use in the <a href="http://codex.wordpress.org/Class_Reference/WP_Query" target="_blank">WP_Query request</a> and as such is very flexible even allowing this widget to show pages rather than posts, show oldest posts first, or filter posts by tag or category. The WP_Query options must be specified in valid JSON format (validate here <a href="http://jsonlint.com/">http://jsonlint.com/</a>).</p>
<p><strong>These are the default args that are sent to WP_Query:</strong></p>
<pre><code class="javascript">{
"post_type": "post",
"posts_per_page": <strong>&lt;same as Number of posts setting&gt;</strong>, 
"orderby": "date",
"order": "DESC"
}
</code></pre>
<p><strong>List of options:</strong></p>
<pre><code class="javascript">{
"post_type": "post", //post, page, revision, attachment, any
"post_status": "publish", //publish, pending, draft, auto-draft, future, private, inherit, trash, any.
"posts_per_page": "5", //overrides number of posts setting
"offset": "3", //number of posts to skip over 
"post__in": [2, 3, 4], //only show posts 2, 3 and 4
"post__not_in": [2, 3, 4], //exclude posts 2, 3 and 4
"ignore_sticky_posts": "1", //ignore sticky posts or not. Default value is 0, don't ignore (requires WordPress 3.1+).
"caller_get_posts": "1", //ignore sticky posts or not. Deprecated as of Version 3.1 in favor of 'ignore_sticky_posts'.
"author": "1", //author ID
"author_name": "john", //post author's name, use 'user_nicename' (NOT name)
"orderby": "date", //none, ID, author, title, date, modified, parent, rand, comment_count, meta_value, meta_value_num  
"order": "DESC", //ASC or DESC
"tag": "cooking,baking", //display posts that have "either" of these tags
"tag": "bread+baking+recipe", //display posts that have "all" of these tags
"tag_id": "13",
"cat": "1, 2, -3", //category ID 1, 2 but not 3
"category_name": "articles,news",
"year": "2011",
"meta_query" : [] //see below
}</code></pre>
<p>For a full listing see <a href="http://codex.wordpress.org/Class_Reference/WP_Query" target="_blank">http://codex.wordpress.org/Class_Reference/WP_Query</a></p>
<h4>Ordering by custom fields</h4>
<p>You can change the sort order to be based on a specified custom field using <em>meta_value</em> and <em>meta_value_num</em> as the <em>orderby</em> option. </p>
<p><em>Note: The difference between meta_value and meta_value_num is that the meta_value_num is sorted as a numeric value (i.e. meta_value would be ordered as a string e.g. 1, 3, 34, 4, 56, 6 whereas meta_value_num would be 1, 3, 4, 6, 34, 56).</em></p>
<p>The following example would order posts by custom field <em>rating</em> showing in order of highest rated to lowest rated.</p>
<pre><code class="javascript">{
     "meta_key": "rating",
     "order": "DESC",
     "orderby": "meta_value_num"
}</code></pre>
<h4>Utilizing the meta_query option</h4>
<p><em>Note: This option requires WordPress 3.1+</em></p>
<p>Filter or show posts with a certain custom field value using the meta_query option. </p>
<p>The following example would display posts with a custom field called <em>mood</em> with a value of <em>happy</em>:</p>
<pre><code class="javascript">{
    "meta_query": [
        {
           "key": "mood", //Custom field key
           "value": "happy", //Custom field value
           "type": "CHAR", //Custom field type. Possible values are 'NUMERIC', 'BINARY', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', 'UNSIGNED'
           "compare": "=" //Operator to test. Possible values are '=', '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'
        }
    ]
}</code></pre>
<p>The following example would display posts if they have a custom field <em>rating</em> of between <em>5 and 10</em> or if they have a custom field <em>mood</em> with a value of <em>happy</em>:</p>
<pre><code class="javascript">{
    "meta_query": {
        "relation": "OR", //Possible values are 'AND', 'OR'
        "0": {
            "key": "rating",
            "value": [5, 10],
            "type": "NUMERIC",
            "compare": "BETWEEN"
        },
        "1": {
            "key": "mood",
            "value": "happy",
            "type": "CHAR",
            "compare": "="
        }
    }
}</code></pre>
<h3>12: Widget output template</h3>
<p>This is an advanced option that modifies the output of the widget. You can modify the output with template tags and add custom classes or tags for styling.</p>
<p>By default the widget outputs posts in this format:</p>
<pre><code class="html">&lt;li&gt;
{THUMBNAIL}
&lt;a title=&quot;{TITLE_RAW}&quot; href=&quot;{PERMALINK}&quot;&gt;{TITLE}&lt;/a&gt;
{EXCERPT}
&lt;/li&gt;</code></pre>
<p>You can modify the output by including the following template tags:</p>
<pre><code class="no-highlight">{ID} The post ID
{THUMBNAIL} The post thumbnail IMG tag if it exists, otherwise nothing
{TITLE_RAW} The post title
{TITLE} The post title, will be trimmed if it exceeds "Option 6: Limit post title chars"
{EXCERPT_RAW} The post excerpt
{EXCERPT} The post excerpt, will be trimmed if it exceeds "Option 7: Limit post excerpt chars"
{PERMALINK} The post permalink url
{DATE} The post date, the format is defined by the "Option 9: Post date format" setting
{DATE[php_date_format]} As of 1.0.4: Customized date format which overrides the default date format setting. E.g. {DATE[l jS F Y]} would display like "Monday 1st January 2011"
{AUTHOR} The post author name
{AUTHOR_LINK} The post author website url
{AUTHOR_AVATAR} As of 1.0.7: Displays the author's avatar if available
{COMMENT_COUNT} The total number of comments, Trackbacks, and Pingbacks for the post
{META[name]} As of 1.0.5: Displays the value of the specified custom field. E.g. {META[Currently Reading]} or {META[Rating]}. Note: Only returns the first result.
{ELLIPSIS}...{/ELLIPSIS} As of 1.0.9: Overrides option "8: limit ellipsis" for the excerpt. 
                         You can use other templates tags within the {ELLIPSIS} tags but not PHP and it will only be appended to the excerpt if it is truncated. 
                         Example: {ELLIPSIS}... &lt;a href=&quot;{PERMALINK}&quot;&gt;(more)&lt;/a&gt;{/ELLIPSIS}
</code></pre>
<h4>Using raw PHP in the Widget output template</h4>
<p>As of 1.0.7 you can add raw PHP code to the widget output template.</p>
<p><em>Note: This will require the PHP code you enter to be run though eval which will likely have a small performance penalty. However if you don't enter any PHP code eval will not be run. Be careful not to give untrusted users permission to modify widget settings since it could be a potential security risk.</em></p>
<p>The following example displays the author's avatar at 32px size:</p>
<pre><code class="php">&lt;li&gt;
&lt;?php echo get_avatar(get_the_author_meta('user_email'), 32); ?&gt;
&lt;a title=&quot;{TITLE_RAW}&quot; href=&quot;{PERMALINK}&quot;&gt;{TITLE}&lt;/a&gt;
{EXCERPT}
&lt;/li&gt;</code></pre>
<p>You can access some of the existing template tag values with the $widget_ouput_template_params array e.g. $widget_ouput_template_params['{ID}'] contains the current posts ID.</p>
<hr />
<h2>Examples of Styling the Output</h2>
<p>The look of the widget output depends a lot on what theme you are using. By default the output looks somewhat like the image below. Thankfully we can improve this with very little effort and a few additions to your themes stylesheet.</p>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2011/08/rpp-example1.jpg" alt="" title="rpp-example1" width="232" height="328" class="aligncenter size-full wp-image-516" />
<p>The first thing you need to do is go to Appearance -> Editor and select the Stylesheet option on the right (it should be selected by default but just make sure). After that paste one of the following styles at the bottom of the stylesheet depending on the look you want. </p>
<p><em>Note these are just basic examples you can do a lot more with more with CSS. These examples also depend a lot on the theme you are using, more complex themes may require additional styling. Hopefully these examples can point you in the right direction.</em></p>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2011/08/rpp-example2.jpg" alt="" title="rpp-example2" width="232" height="334" class="aligncenter size-full wp-image-517" />
<pre><code>.widget_recent_entries li {
    clear: both;
    margin: 5px 0;
}
.widget_recent_entries .wp-post-image {
    vertical-align: text-top;
    margin: 0 5px 0 0;
}</code></pre>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2011/08/rpp-example3.jpg" alt="" title="rpp-example3" width="255" height="316" class="aligncenter size-full wp-image-520" />
<pre><code>.widget_recent_entries li {
    clear: both;
    margin: 15px 0;
}

.widget_recent_entries .wp-post-image {
    float: left;
    margin: 0 5px 0 0;
}

.widget_recent_entries li p {
    margin-top: 5px;
    color: #707070;
}</code></pre>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2011/08/rpp-example4.jpg" alt="" title="rpp-example4" width="232" height="220" class="aligncenter size-full wp-image-518" />
<pre><code>.widget_recent_entries li {
    clear:both;
    margin: 5px 0;
}

.widget_recent_entries .wp-post-image {
    vertical-align: middle;
    margin: 0 5px 0 0;
}</code></pre>
<p>The above example has the thumbnail size set to 25px x 25px.</p>
<img src="http://www.pjgalbraith.com/wp-content/uploads/2011/08/rpp-example5.jpg" alt="" title="rpp-example5" width="232" height="430" class="aligncenter size-full wp-image-519" />
<pre><code>.widget_recent_entries li {
    clear:both;
    margin: 10px 0;
    text-align: center;
}

.widget_recent_entries .wp-post-image {
    margin: 0 5px 0 0;
}

.widget_recent_entries li a {
    display: block;
}</code></pre>
<p>The above example has the thumbnail size set to 100px x 100px.</p>
]]></content:encoded>
			<wfw:commentRss>https://www.pjgalbraith.com/recent-posts-plus/feed/</wfw:commentRss>
		<slash:comments>358</slash:comments>
		</item>
	</channel>
</rss>
