Reading List

The most recent articles from a list of feeds I subscribe to.

Refresh CSS Bookmarklet v2

Almost 11 years ago, Paul Irish posted this brilliant bookmarklet to refresh all stylesheets on the current page. Despite the amount of tools, plugins, servers to live reload that have been released over the years, I’ve always kept coming back to it. It’s incredibly elegant in its simplicity. It works everywhere: locally or remotely, on any domain and protocol. No need to set up anything, no need to alter my process in any way, no need to use a specific local server or tool. It quietly just accepts your preferences and workflow instead of trying to change them. Sure, it doesn’t automatically detect changes and reload, but in most cases, I don’t want it to.

I’ve been using this almost daily for a decade and there’s always been one thing that bothered me: It doesn’t work with iframes. If the stylesheet you’re editing is inside an iframe, tough luck. If you can open the frame in a new tab, that works, but often that’s nontrivial (e.g. the frame is dynamically generated). After dealing with this issue today once more, I thought “this is just a few lines of JS, why not fix it?”.

The first step was to get Paul’s code in a readable format, since the bookmarklet is heavily minified:

(function() {
	var links = document.getElementsByTagName('link');
	for (var i = 0; i < links.length; i++) {
		var link = links[i];
		if (link.rel.toLowerCase().match(/stylesheet/) && link.href) {
			var href = link.href.replace(/(&|%5C?)forceReload=\d+/, '');
			link.href = href + (href.match(/\?/) ? '&' : '?') + 'forceReload=' + (new Date().valueOf())
		}
	}
})()

Once I did that, it became obvious to me that this could be shortened a lot; the last 10 years have been wonderful for JS evolution!

(()=>{
	for (let link of Array.from(document.querySelectorAll("link[rel=stylesheet][href]"))) {
		var href = new URL(link.href, location);
		href.searchParams.set("forceReload", Date.now());
		link.href = href;
	}
})()

Sure, this reduces browser support a bit (most notably it excludes IE11), but since this is a local development tool, that’s not such a big problem.

Now, let’s extend this to support iframes as well:

{
	let $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector));

	let refresh = (document) => {
		for (let link of $$("link[rel=stylesheet][href]", document)) {
			let href = new URL(link.href);
			href.searchParams.set("forceReload", Date.now());
			link.href = href;
		}

		for (let iframe of $$("iframe", document)) {
			iframe.contentDocument && refresh(iframe.contentDocument);
		}
	}

	refresh();
}

That’s it! Do keep in mind that this will not work with cross-origin iframes, but then again, you probably don’t expect it to in that case.

Now all we need to do to turn it into a bookmarklet is to prepend it with javascript: and minify the code. Here you go:

[Refresh CSS](javascript:{let e=(e,t=document)=>Array.from(t.querySelectorAll(e)),t=r=>{for(let t of e(‘link[rel=stylesheet][href]’,r)){let e=new URL(t.href);e.searchParams.set(‘forceReload’,Date.now()),t.href=e}for(let o of e(‘iframe’,r))o.contentDocument&&t(o.contentDocument)};t()})

Hope this is useful to someone else as well :) Any improvements are always welcome!

Credits

  • Paul Irish, for the original bookmarklet
  • Maurício Kishi, for making the iframe traversal recursive (comment)

Refresh CSS Bookmarklet v2

Almost 11 years ago, Paul Irish posted this brilliant bookmarklet to refresh all stylesheets on the current page. Despite the amount of tools, plugins, servers to live reload that have been released over the years, I’ve always kept coming back to it. It’s incredibly elegant in its simplicity. It works everywhere: locally or remotely, on any domain and protocol. No need to set up anything, no need to alter my process in any way, no need to use a specific local server or tool. It quietly just accepts your preferences and workflow instead of trying to change them. Sure, it doesn’t automatically detect changes and reload, but in most cases, I don’t want it to.

I’ve been using this almost daily for a decade and there’s always been one thing that bothered me: It doesn’t work with iframes. If the stylesheet you’re editing is inside an iframe, tough luck. If you can open the frame in a new tab, that works, but often that’s nontrivial (e.g. the frame is dynamically generated). After dealing with this issue today once more, I thought “this is just a few lines of JS, why not fix it?”.

The first step was to get Paul’s code in a readable format, since the bookmarklet is heavily minified:

(function() {
	var links = document.getElementsByTagName('link');
	for (var i = 0; i < links.length; i++) {
		var link = links[i];
		if (link.rel.toLowerCase().match(/stylesheet/) && link.href) {
			var href = link.href.replace(/(&|%5C?)forceReload=\d+/, '');
			link.href = href + (href.match(/\?/) ? '&' : '?') + 'forceReload=' + (new Date().valueOf())
		}
	}
})()

Once I did that, it became obvious to me that this could be shortened a lot; the last 10 years have been wonderful for JS evolution!

(()=>{
	for (let link of Array.from(document.querySelectorAll("link[rel=stylesheet][href]"))) {
		var href = new URL(link.href, location);
		href.searchParams.set("forceReload", Date.now());
		link.href = href;
	}
})()

Sure, this reduces browser support a bit (most notably it excludes IE11), but since this is a local development tool, that’s not such a big problem.

Now, let’s extend this to support iframes as well:

{
	let $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector));

	let refresh = (document) => {
		for (let link of $$("link[rel=stylesheet][href]", document)) {
			let href = new URL(link.href);
			href.searchParams.set("forceReload", Date.now());
			link.href = href;
		}

		for (let iframe of $$("iframe", document)) {
			iframe.contentDocument && refresh(iframe.contentDocument);
		}
	}

	refresh();
}

That’s it! Do keep in mind that this will not work with cross-origin iframes, but then again, you probably don’t expect it to in that case.

Now all we need to do to turn it into a bookmarklet is to prepend it with javascript: and minify the code. Here you go:

🔄 CSS v2

Hope this is useful to someone else as well :) Any improvements are always welcome!

Credits

  • Paul Irish, for the original bookmarklet
  • Maurício Kishi, for making the iframe traversal recursive (comment)

Easy Dynamic Regular Expressions with Tagged Template Literals and Proxies

If you use regular expressions a lot, you probably also create them from existing strings that you first need to escape in case they contain special characters that need to be matched literally, like $ or +. Usually, a helper function is defined (hopefully this will soon change as RegExp.escape() is coming!) that basically looks […]

Easy Dynamic Regular Expressions with Tagged Template Literals and Proxies

If you use regular expressions a lot, you probably also create them from existing strings that you first need to escape in case they contain special characters that need to be matched literally, like $ or +. Usually, a helper function is defined (hopefully this will soon change as RegExp.escape() is coming!) that basically looks […]

Easy Dynamic Regular Expressions with Tagged Template Literals and Proxies

If you use regular expressions a lot, you probably also create them from existing strings that you first need to escape in case they contain special characters that need to be matched literally, like $ or +. Usually, a helper function is defined (hopefully this will soon change as RegExp.escape() is coming!) that basically looks like this:

var escapeRegExp = s => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");

and then regexps are created by escaping the static strings and concatenating them with the rest of the regex like this:

var regex = RegExp(escapeRegExp(start) + '([\\S\\s]+?)' + escapeRegExp(end), "gi")

or, with ES6 template literals, like this:

var regex = RegExp(`${escapeRegExp(start)}([\\S\\s]+?)${escapeRegExp(end)}`, "gi")

(In case you were wondering, this regex is taken directly from the Mavo source code)

Isn’t this horribly verbose? What if we could define a regex with just a template literal (`${start}([\\S\\s]+?)${end}` for the regex above) and it just worked? Well, it turns out we can! If you haven’t seen tagged template literals before, I suggest you click that MDN link and read up. Basically, you can prepend an ES6 template literal with a reference to a function and the function accepts the static parts of the string and the dynamic parts separately, allowing you to operate on them!

So, what if we defined such a function that returns a RegExp object and escapes the dynamic parts? Let’s try to do that:

var regexp = (strings, ...values) => {
	return RegExp(strings[0] + values.map((v, i) => escapeRegExp(v) + strings[i+1]).join(""))
};

And now we can try it in the console:

> regexp`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//

Won’t somebody, please, think of the flags?!

This is all fine and dandy, but how do we specify flags? Note that the original regexp had flags (“gi”). The tagged template syntax doesn’t really allow us to pass in any additional parameters. However, thanks to functions being first-class objects in JS, we can have a function that takes the flags in as parameters and returns a function that generates regexps with the right flags:

var regexp = flags => {
	return (strings, ...values) => {
		var pattern = strings[0] + values.map((v, i) => escapeRegExp(v) + strings[i+1]).join("")
		return RegExp(pattern, flags);
	}
};

And now we can try it in the console:

> regexp("gi")`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//gi

This works nice, but now even if we don’t want any flags, we can’t use the nice simple syntax we had earlier, we need to include a pair of empty parens:

> regexp()`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//

Can we have our cake and eat it too? Can we have the short parenthesis-less syntax when we have no flags, and still be able to specify flags? Of course! We can check the arguments we have and either return a function, or call the function. If our function is used as a tag, the first argument will be an array (thanks Roman!). If we’re expecting it to return a function, the first argument would be a string: the flags. So, let’s try this approach!

var regexp = (...args) => {
	var ret = (flags, strings, ...values) => {
		var pattern = strings[0] + values.map((v, i) => escapeRegExp(v) + strings[i+1]).join("");
		return RegExp(pattern, flags);
	};

	if (Array.isArray(args[0])) {
		// Used as a template tag
		return ret("", ...args);
	}

	return ret.bind(undefined, args[0]);
};

And now we can try it in the console and verify that both syntaxes work:

> regexp("gi")`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//gi
regexp`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//

Even nicer syntax, with proxies!

Is there a better way? If this is not super critical for performance, we could use proxies to return the right function with a template tag like regexp.gi, no parentheses or quotes needed and the code is actually shorter too:

var _regexp = (flags, strings, ...values) => {
	var pattern = strings[0] + values.map((v, i) => escapeRegExp(v) + strings[i+1]).join("");
	return RegExp(pattern, flags);
};
var regexp = new Proxy(_regexp.bind(undefined, ""), {
	get: (t, property) => _regexp.bind(undefined, property)
});

And now we can try it in the console, both with and without flags!

> regexp.gi`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//gi
> regexp`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//

That’s some beauty right there! ?

PS: If you liked this, take a look at this mini-library by Dr. Axel Rauschmayer that uses a similar idea and turns it into a library that does more than just escaping strings (different syntax for flags though, they become part of the template string, like in PHP)