Skip to content

String Replacement in PHP

Working with Parsedown, I want to string manipulation but only in certain parts. For instance, on text not in HTML tags or not in quotes. The right way to do that is with a real parser. The easy way is by removing the unwanted strings, replacing them with a marker that won't come up in normal text, doing the manipulation, then replacing the markers (it is the replacement step that requires "a marker that won't come up in normal text"; you don't want to replace text that was present in the original).

I would use a marker that can't be typed but still is legal HTML; turns out that U+FFFC (OBJECT REPLACEMENT CHARACTER, ) is perfect for that. So I made a pair of functions, `StringReplace\remove` and `StringReplace\restore` to make that easy.

StringReplace\remove ($re, $target)
Any string that matches the regular expression $re in $target is replaced by a numbered marker, "{number}". The new string is returned. So for instance,

$rawtext = StringReplace\remove ('#</?[^>]*>#', $html);

will remove tags.

StringReplace\restore ($target)
Returns a string with the markers replaced by their original versions.

The code

namespace StringReplace;

define ('OBJECT_REPLACEMENT_CHARACTER', '');
define ('RE_REPLACEMENT', '/'.OBJECT_REPLACEMENT_CHARACTER.'(\d+)'.OBJECT_REPLACEMENT_CHARACTER.'/');

$strings = array();

$remover = function ($matches){
  global $strings;
  $strings []= $matches[0];
  return OBJECT_REPLACEMENT_CHARACTER.count($strings).OBJECT_REPLACEMENT_CHARACTER;
};

$replacer = function ($matches){
  global $strings;
  return $strings[$matches[1]-1];
};

function remove ($re, $target){
  global $remover;
  return preg_replace_callback ($re, $remover, $target);
}

function restore ($target){
  global $replacer;
  return preg_replace_callback (RE_REPLACEMENT, $replacer, $target);
}

Extending Parsedown

I've been spending all my intellectual free time on working on my Kavanot site, so I haven't been doing any independent programming. But that site uses raw HTML, which is a pain to type. So I decided to start using Markdown to make writing easier. After a little trial and error, I decided to use Parsedown with Parsedown Extra.

See the code.

Continue reading ›

H&R Block Updating Errors

(It's been almost a year since I've posted. My intellectual life is busy with other things)

I use H&R Block software to prepare my taxes and have been generally very happy, but this year it would not update the program after January 1. Using the automatic update downloaded the updater but it silently failed. Manually downloading from http://www.hrblock.com/tax-software/updates-back-editions/federal-windows.html then running that would run WinZip to unpack it, then silently fail. The tech support at H&R Block was useless.

Some Google-ing led to this solution:

  1. Run the update installer from the hrblock.com site as though it was going to work
  2. Wait for it to fail
  3. Find the unpacked downloaded installer:
    1. Windows-R key then type %temp% to open the temporary download folder
    2. Find the most recent folder (it will have a GUID-type name, like {deaf-face-1234}. Open it
    3. There should be an msp file like H&R Block Deluxe 2016 Update.msp
  4. Double click that to run it
  5. Run H&R Block

That's all it took! Obviously the unpacking worked correctly, so I don't know why the program wouldn't run the actual installer.

‘ob_gzhandler’ conflicts with ‘zlib output compression’

Nearly Free Speech has been a great hosting service, and they upgrade the stack consistently, which usually doesn't cause problems. But with the most recent upgrade, I started getting the above error. Looks like they turned on compression at the server level, so doing it on each page is redundant.

Evidently they announced it on the blog, but I never keep up with that.

Changing all my ob_start('ob_gzhandler'); to ob_start(); fixes it. Hope this helps someone else.

Amazon Wish List Hack on github

I've put an Amazon Wishlist Widget for WordPress on my github site, that uses the techniques described before. You can see it running on the sidebar here.

Getting multiple pages in the Amazon Wish List

I figured out how to get all the pages from screen-scraping the Amazon wish list. Basically, look for the "Next" button (it's in a <li class=a-last> element). If that element is present, look for the next page.

function getwishlistitems ($listID, $page=1){
	// ignore parsing warnings
	$wishlistdom = new DOMDocument();
	@$wishlistdom->loadHTMLFile("http://www.amazon.com/gp/registry/wishlist/$listID?disableNav=1&page=$page");
	$wishlistxpath = new DOMXPath ($wishlistdom);
	$items = iterator_to_array($wishlistxpath->query("//div[starts-with(@id,'item_')]"));
	if ($wishlistxpath->evaluate("count(//li[@class='a-last'])")) { // this is the "Next->" button
		$items = array_merge($items, $this->getwishlistitems($listID, $filter, $page+1));
	}
	return $items;
}

Note that this creates a complication: the array of items now includes nodes from different documents, so you can't use one saved DOMXPath. Instead, where the original code has $wishlistxpath->evaluate($xpath, $node), use

(new DOMXPath($node->ownerDocument))->evaluate($xpath, $node);

Hope this helps someone.

Hacking My Way Again to an Amazon Wishlist Widget

Amazon long ago elminated its API for getting wishlists. 4 years ago I made a screen-scraping WordPress widget to display my wishlist. Unfortunately, as happens with screen-scraping, Amazon changed their format and URL's. And now I can't seem to get the ItemLookup API to work either.

doitlikejustin has a vanilla PHP wishlist scraper, but PHP 5 now has it's own HTML parser in DOMDocument, so I implemented my own.

The wishlist page has a simple structure, and all links to Amazon products have as part of the URL "dp/{ASIN}", where {ASIN} is the Amazon ID number, and all the individual items are contained in <div>s that have an id that starts with "item_", and the title is in a link that has an id that starts with "itemName". The image and author list are in consistent positions relative to those. Other advertisements for Amazon products that you see on the page are added with Javascript, so they won't show up when we grab the page with PHP.

Images URL images have the format "http://ecx.images-amazon.com/images/I/{idcode}._SL{size}.jpg" (with possibly some extra parameters before the "SL"). I just
pull the relevant idcode out and create my own URL with the desired size.

function wishlist($listID){
	$size = 100;
	$ret = array();
	$wishlistdom = new DOMDocument();
	// ignore parsing warnings
	@$wishlistdom->loadHTMLFile("http://www.amazon.com/gp/registry/wishlist/$listID?disableNav=1");
	$wishlistxpath = new DOMXPath ($wishlistdom);
	// I want to be able to limit and rearrange the list, so I turn it into an array
	$items = iterator_to_array($wishlistxpath->query("//div[starts-with(@id,'item_')]"));
	// filter $items as desired, then pull out the data
	foreach ($items as $item){
		$link = $wishlistxpath->evaluate(".//a[starts-with(@id, 'itemName')]", $item)->item(0);
		$href = $link->attributes->getNamedItem('href')->nodeValue;
		if (preg_match ('|/dp/\w+|', $href, $matches)){
			$href = "http://amazon.com$matches[0]"; // simplify the URL
		}else{
			$href = "http://amazon.com$href";
		}
		$title = $link->textContent;
		$author = $link->parentNode->nextSibling->textContent;
		$image = $wishlistxpath->query(".//img", $item)->item(0)->attributes->getNamedItem('src')->nodeValue;
		if (preg_match ('|http://ecx.images-amazon.com/images/I/[^.]+|', $image, $matches)){
			$image = $matches[0]."._SL$size.jpg";
		}else{
			$image = "http://ecx.images-amazon.com/images/G/01/x-site/icons/no-img-sm._SL${size}_.jpg";
		}
		$image = "<img src='$image' alt='$title'><br/>";
		$ret[] = "<a href='$href'>$image$title<br/>$author</a>";
	}
	return ret;
}

Now this only gets the first page (25 items) of a wish list. I modified it to allow finding all the items on a wish list.

Odd bug with Date’s

It's been almost a year since I last posted. I'm still programming, but it's mostly visible on github, especially my trying to help with jquery/globalize by implementing nongregorian calendars.

I finally solved a bug that was, um, bugging me. In order to test my Julian Day routines, I needed to create a javascript Date at midnight UTC rather than local time. I thought I was clever when I did:

d = new Date();
d.setUTCFullYear( year );
d.setUTCMonth( month );
d.setUTCDate( date );
d.setUTCHours( 0 );
d.setUTCMinutes( 0 );
d.setUTCSeconds( 0 );

And everything worked fine, until last night when I would set month=1 (February) but the month would end up as 2 (March). This had never happened before, and the code hadn't changed.

I finally realized that the date was 2016-01-28 St. Louis time at 2300, or 2016-01-29 UTC. So setting the year to a non-leap year like 2015, then setting the month to 1 with setUTCMonth() meant I was trying to set it to 2015-02-29, which Date helpfully corrected to 2015-03-01, then the date was set correctly. The only way that bug would turn up is if my code was run on the last day of the month and tested with a shorter month.

Turns out there's a much better way to set UTC time:

d = new Date( Date.UTC( year, month, date ) )

And now I am wiser.

New flexcal

After some prompting by those actually using it, I paid more attention to my flexcal date picker plugin, adding features like buttons, drop-down menus and formatted dates. The documentation is on my github pages at github.bililite.com/flexcal, and the code is on github at github.com/dwachss/flexcal. All other posts about it are obsolete.

The current stable version is 3.4.

Animating arbitrary values with jQuery

jQuery's animate is useful for animating CSS properties, but there are times that you want to take advantage of the bookkeeping that jQuery provides to animate, with easing functions etc., but not to change an animatable property. The flip plugin for Mike Alsup's cycle2 has a clever hack to animate some otherwise unused property instead:


<input type=button value="Click Me" id=rotatebutton />
<div id=rotatetest style="height: 100px; width: 100px; background: #abcdef" >Whee!</div>

$('#rotatebutton').click(function(){
  $('<div>'). // create an unused element (could use a preexisting one to save some time and memory)
    animate({lineHeight: 360}, // we aren't really using lineHeight; we just want something numeric.
      // We want it to go from 0 (the default for anything that has a value of 'auto') to 360
    {
      duration: 2000,
      easing: 'easeOutElastic', // use jQuery and jQuery UI to manage the animation timing
      step: function (now){
        $('#rotatetest').css('transform', 'rotate('+now+'deg)'); // use the number that animate gives us
      }
    });
});