This note describes how to write expandfile macros.
You define HTMX macros using a *block, and invoke them with *callv. Macros can be called with any number of arguments: inside the macro body, the macro accesses the arguments as param1, param2, and so on. The value _xf_n_callv_args is set before expansion to the number of arguments.
Variable and builtin references in the macro block are expanded, just like *expand, when the macro is invoked by *callv. Macro invocations can either write output, set variables, or both. Macros can call other macros and even call themselves, since the paramN values are saved at the start of invocation and restored afterward.
Using macros lets you figure out a tricky piece of implementation once, and use it many times. Instead of copying and pasting HTMX statements, you can refer to them with *callv; this will make your HTMX source files smaller, and avid errors in pasting and inserting arguments. If you want to change the implementation later, to add a feature or fix a bug, you can change your macro and recompile, instead of having to edit many places where it was pasted.
Document Specific Macros
Sometimes a particular document will contain a list of items that all need similar formatting, and it will be worth writing a small macro just for that one file. You can define such macros in *block statements and invoke them with *callv. If the macro turns out to be useful in several documents, you can move the *block defining it to a file that is included in all the documents that need it.
Macro Libraries
The file htmxlib.htmi supplied with expandfile provides useful macros. Look at the macro library source for some examples of HTMX coding.
Macro | args | Function |
---|---|---|
getimgtag | path,alttag,titletag | Output a responsive IMG tag |
getimgdiv | path,target,alttag,titletag,class,caption | Output a DIV wrapping a responsive IMG tag |
getimgpopdiv | path,target,alttag,titletag,class,thumbcaption,popupcaption | Output a DIV wrapping a responsive IMG tag with a popup image |
getimgtag75 | path,alttag,titletag | output an IMG tag of size 75x75 |
getimgdiv75 | path,target,alttag,titletag,class,caption | output a DIV wrapping an IMG tag of size 75x75 with a caption |
getimgpopdiv75 | path,target,alttag,titletag,class,thumbcaption,popupcaption | output a DIV wrapping an IMG tag of size 75x75 with a caption and a popup image |
getfancybox_li | dividtag,thumbnailsdir,jpgfile,caption[,imgdir] | output an LI tag with a responsive IMG for use with fancybox jquery plugin |
lightbox_li | dividtag,thumbnailsdir,jpgfile,caption | output an LI tag for use with fancybox jquery plugin |
gettwodigit | number | return the conversion of numeric argument to two digits |
getformattedsecs | numsecs | return the conversion of argument in seconds to formatted time hh:mm:ss |
getcomma3 | number | return the conversion of numeric argument formatted with commas every 3 digits |
getapplemenu | textstring | return text string wrapped in a SPAN with class macmenu |
imgtag | path,alttag,titletag | set imgtag_result to an IMG tag |
imgdiv | path,target,alttag,titletag,class,caption | set imgtag_result to a DIV tag with caption |
imgpopdiv | path,target,alttag,titletag,class,thumbcaption,popupcaption | return a DIV tag with caption |
twodigit | number | set x0 to conversion of integer argument to 2 digits |
setscale | number | set x0 to conversion of integer argument to scaled number of bytes |
dumpsql | dump the values bound by *sqlloop statement to an html formatted dump | |
javatag | classfile,name,width,height,title,sorrymsg,standbymsg,paramblockname | output an APPLET tag |
headinginit | set up heading macros | |
heading2 | title | increment level2 and output an H2 tag and add a line to toc |
heading3 | title | increment level3 and output an H3 tag and add a line to toc |
heading4 | title | increment level4 and output an H4 tag and add a line to toc |
wrap | variable,prefix,suffix | if variable is nonempty, return its value surrounded by prefix and suffix |
firstnonempty | var1,var2,var3,... | examine each argument in turn and return the first nonempty value |
Example: setscale
Here is a small macro that converts a number of bytes into a human-friendly number. It is included in htmxlib.htmi, but I don't use it very often. This macro converts a number of bytes to a human readable measure. Its output is in two variables, x0 and y0.
Source code for setscale
%[** ================================================================ **]% %[** convert bytes to appropriate scale **]% %[** *callv,setscale,number **]% %[** Parameters: **]% %[** - param1 = count of bytes **]% %[** Return: **]% %[** - x0 = int(param1/1024); y0 = "KB"; z0 = 1024 etc **]% %[** - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - **]% %[*block,&setscale,^END$]% %[*set,&x0,param1]% %[*set,&y0,=""]% %[*set,&z0,=1]% %[*if,eq,x0,="",*set,&x0,=0]% %[*if,ge,x0,=512,*set,&y0,="KB"]% %[*if,ge,x0,=512,*product,&z0,z0,=1024]% %[*if,ge,x0,=512,*quotientrounded,&x0,x0,=1024]% %[*if,ge,x0,=512,*set,&y0,="MB"]% %[*if,ge,x0,=512,*product,&z0,z0,=1024]% %[*if,ge,x0,=512,*quotientrounded,&x0,x0,=1024]% %[*if,ge,x0,=512,*set,&y0,="GB"]% %[*if,ge,x0,=512,*product,&z0,z0,=1024]% %[*if,ge,x0,=512,*quotientrounded,&x0,x0,=1024]% %[*if,ge,x0,=512,*set,&y0,="TB"]% %[*if,ge,x0,=512,*product,&z0,z0,=1024]% %[*if,ge,x0,=512,*quotientrounded,&x0,x0,=1024]% END
Programming Observations
This macro sets three variables, x0, y0, z0. Maybe I should have named them _setscale_num, _setscale_units, _setscale_factor to avoid destroying user variables.
*quotientrounded is built in to expandfile but setscale is a macro. I could have made both builtins, or both macros; this seemed like the right balance.
Example: getimgdiv
The HTML statement to place an image on a web page looks like <IMG SRC="thing.jpg" WIDTH="949" HEIGHT="648">. You can omit the WIDTH and HEIGHT for the image, but then the browser has to wait to load the image and find out its size, so the page takes longer to load. In order to include WIDTH and HEIGHT in the IMG tag, you have to look up the dimensions of the picture as you type. expandfile provides macros that let you just name the image in your HTMX source, and then fill in the image dimensions at compile time, when generating HTML from HTMX. (This requires that the image file be available at compile time as well as page display time.)
The getimgdiv macro, included in htmxlib.htmi, returns a DIV element containing an IMG tag.
%[*callv,getimgdiv,imagepath,target,alt,title,class,caption]%
The call specifies an anchor graphic,
an optional target which will be linked to,
an optional alt tag for the anchor graphic,
an optional title tag for the anchor graphic,
an optional CSS class for the DIV, and
an optional caption for the DIV.
getimgdiv outputs a DIV which contains an IMG tag and a caption (if supplied). If the CSS class is specified, it is included as a CLASS attribute of the DIV. If the graphic is found, the DIV will have a WIDTH attribute specified that is as wide as the graphic. (These two features help with page layout: for example, the CSS class can cause the graphic to float.) The graphic is referenced by an IMG tag that includes the WIDTH and HEIGHT of the graphic, the ALT tag, and the TITLE tag. If the target is specified, the IMG tag is surrounded by an A tag that links to the target. So if path refers to a 949x648 pixel image, you can write
This is very handy already, but there's more.
High Pixel Density Screens
Web pages are designed assuming that 96 pixels (CSS pixels) are one inch wide, but many newer devices provide many more pixels: for example, Retina Macintoshes have 264 physical pixels per inch (ppi) and iPhones have 326 ppi.
Standard advice for coding HTML is to always specify WIDTH and HEIGHT (in CSS pixels) on an IMG tag, so that page layout does not need to wait for the image to be read in to determine its size.
if WIDTH and HEIGHT are specified in an IMG tag, and the actual pixel dimensions of an image are different, Web browsers must stretch or shrink the image to fit into the specified size. When modern browsers do this, they use information about your screen's actual device resolution. When Web browsers display an image by using more device pixels per CSS pixel, the result looks fuzzy compared to text rendered using all the available pixels. (For example, if you have a photo that is 96x96 pixels, the HTML layout rules say this should occupy 1 inch by 1 inch on the screen. To display this image on a Retina screen, where 1 inch is 264 pixels, the browser must use about 4 device pixels per 1 image pixel. This will look unsharp.)
To avoid the fuzziness, we can give a Web browser an image file with more image pixels, and tell the browser to fit the picture into a CSS space with fewer CSS pixels. Modern browsers are smart enough to use the "extra" image pixels to make the display look sharper.
Here is an example. The left image below is 75x75 image pixels displayed in a 75x75 CSS pixel space (1851 bytes). The right is 150x150 image pixels displayed in a 75x75 CSS pixel space (5565 bytes). On a 96 DPI display, the two images will look identical. On a high DPI display, the -2x version will look sharper.
<img src="thumb-t1.jpg"
alt="thumb-t1" width="75" height="75">
<img src="thumb-t1-2x.jpg"
alt="thumb-t1-2x" width="75" height="75">
There is a cost to sending larger pictures: we want don't to send big images to a device with low ppi, and then have the browser discard most of the data we sent, at a cost in speed and wasted bandwidth. In 2014, this problem was fixed by adding new HTML features to IMG tags to specify which of several graphic files to load, depending on the monitor's pixel density and size, when several different image sizes are available. In addition to writing IMG SRC=xxx, we specify the SRCSET attribute, listing several different file names and the pixel density appropriate for each. Look it up.
Generating IMG Tags
Instead of fiddling with complicated new HTML attributes, you can use an Expandfile macro.
getimgdiv hides the complications of handling multiple files and SRCSET attributes by
- Defining a naming convention for different file sizes.
- Checking for the existence of higher pixel count files.
- When possible, adding a SRCSET= attribute to the IMG tag it generates.
- Creating a DIV containing the IMG tag and the caption.
- Optionally making the image a hyperlink to a specified destination.
All you have to do is generate your image references with getimgdiv and provide multiple versions of graphic files. GIF, JPG, and PNG files are all supported. A modern Web browser will pick the best one. The result will be better looking web pages.
How It Works
getimgdiv checks to see if there are multiple versions of the same image file with different pixel densities. If it is displaying img1.jpg, it will check for the existence of img1-2x.jpg. To do this, getimgdiv uses the *shell builtin to invoke a helper program called gifsize2, which is supplied with expandfile. This program is invoked with a graphic file name and an optional suffix. It returns the dimensions of the graphic in pixels, and the file name inclding the suffix. If the suffixed graphic file does not exist, gifsize2 returns an empty line. For example:
$ gifsize2 img1.jpg 75 75 img1.jpg $ gifsize2 img1.jpg 2x 150 150 img1-2x.jpg $ gifsize2 img1.jpg 3x
getimgdiv looks for the specified file name, and also the file name with the -2x suffix, and if both are available, generates a DIV with a SRC= attribute naming the specified file and a WIDTH= for the file's width, and a SRCSET attribute specifying the "1x" and "2x" file versions.
getimgdiv handles several alternate cases:
- If only the named image is found, use it in the SRC= of an IMG tag and generate the WIDTH= and HEIGHT= from the file size, with no SRCSET=.
- If the named image and a -2x version are both found, use the named image in the SRC= of an IMG tag and generate a SRCSET= referencing both images.
- If the named image is not found, but a -2x version is found, create an IMG tag with a SRC= referencing the -2x file and a WIDTH= and HEIGHT= half the size.
- If neither the named image nor a -2x version is found, generate the DIV and IMG without dimensions, so that it will work if the named image is supplied later. In this case, print a warning at compilation time.
- If "tgtpath" is specified, wrap the IMG tag in an A tag with an HREF= pointing to the target path.
- If "mycaption" is specified, include it in a P tag with CLASS="caption".
For all cases, getimgdiv generates a DIV tag that encloses the IMG tag. If "myclass" is specified, it is used as the CLASS= attribute of the DIV.
Currently, getimgdiv only looks for the -2x version of files. This code can be extended to handle -3x and -4x as future devices with finer detail screens become common. I tried -3x files on an iPhone and decided that while -2x was a big improvement, -3x was not worth doing.
Source Code of the getimgdiv Macro
%[*block,&doc,^ENDDOC]% ================================================================ macro for generating a RESPONSIVE image DIV with optional caption *callv,getimgdiv,path,target,alttag,titletag,class,caption parameters: param1 - file path, same rel path must work in source and object dir param2 - link target or "" .. if link target is specified, put in a link. since no size is specified, no possibility of 2x param3 - alt tag param4 - title tag param5 - DIV class param6 - caption, may not contain refs like {[ ... ]} directly because they would be expanded to things with commas and generate extra params .. instead of ="{[ ... ]}" use ="%[lbrace]%[ ... ]%[rbrace]%" gets width and height from inside the graphic. if not found, leaves dimensions out. also looks for file-2x.jpg, and if it is found, uses that file, with the size of the base file output: (resulting DIV) (8 cases: 4 cases with no target, 4 cases with target) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ENDDOC %[*block,&getimgdiv,^END]% %[*set,&_class,=""]% %[*if,ne,param5,="",*set,&_class,=" class=\""]% %[*if,ne,param5,="",*concat,&_class,param5]% %[*if,ne,param5,="",*concat,&_class,="\""]% %[*set,&_fmtstring0,="<div$5>$10<img src=\"$1\" border=\"0\" alt=\"$3\" title=\"$4\">$11<p class=\"caption\">$6</p></div>\\n"]% %[*set,&_fmtstring1,="<div$5 style=\"width: $7px;\">$10<img src=\"$1\" width=\"$7\" height=\"$8\" border=\"0\" alt=\"$3\" title=\"$4\">$11$12</div>\\n"]% %[*set,&_fmtstring2,="<div$5 style=\"width: $7px;\">$10<img src=\"$9\" width=\"$7\" height=\"$8\" border=\"0\" alt=\"$3\" title=\"$4\">$11$12</div>\\n"]% %[*set,&_fmtstring3,="<div$5 style=\"width: $7px;\">$10<img src=\"$1\" width=\"$7\" height=\"$8\" border=\"0\" alt=\"$3\" title=\"$4\" srcset=\"$1 1x, $9 2x\">$11$12</div>\\n"]% %[*set,&_beglink,=""]% %[*set,&_endlink,=""]% %[*set,&_fmtstring,=""]% %[*set,&_width,=""]% %[*set,&_height,=""]% %[*set,&_cap,=""]% %[** .. look for 1x version **]% %[*shell,&_gifsizer,=gifsize2 \"%[param1]%\"]% %[*if,eq,_gifsizer,="",*warn,missing: %[param1]%]% %[*if,ne,_gifsizer,="",*set,&_fmtstring,_fmtstring1]% %[*if,ne,_gifsizer,="",*popssv,&_width,&_gifsizer]% %[*if,ne,_gifsizer,="",*popssv,&_height,&_gifsizer]% %[** .. look for 2x version **]% %[*shell,&_gifsizer2x,=gifsize2 \"%[param1]%\" 2x]% %[*if,eq,_gifsizer,="",*if,eq,_gifsizer2x,="",*set,&_fmtstring,_fmtstring0]% %[*if,eq,_gifsizer,="",*if,ne,_gifsizer2x,="",*set,&_fmtstring,_fmtstring2]% %[*if,ne,_gifsizer,="",*if,ne,_gifsizer2x,="",*set,&_fmtstring,_fmtstring3]% %[*if,ne,_gifsizer2x,="",*popssv,&_width2x,&_gifsizer2x]% %[*if,ne,_gifsizer2x,="",*popssv,&_height2x,&_gifsizer2x]% %[*if,eq,_gifsizer,="",*if,ne,_gifsizer2x,="",*quotient,&_width,_width2x,=2]% %[*if,eq,_gifsizer,="",*if,ne,_gifsizer2x,="",*quotient,&_height,_height2x,=2]% %[*if,ne,_gifsizer2x,="",*popssv,&_filename2x,&_gifsizer2x]% %[** .. if param2 is given, set the link **]% %[*if,ne,param2,="",*format,&_beglink,="<a href=\"$1\">",param2]% %[*if,ne,param2,="",*set,&_endlink,="</a>"]% %[** .. if param6 is given, set the caption **]% %[*if,ne,param6,="",*format,&_cap,="<p class=\"caption\">$1</p>",param6]% %[** .. generate the div tag **]% %[*format,&_getimgdiv_result,_fmtstring,param1,param2,param3,param4,_class,param6,_width,_height,_filename2x,_beglink,_endlink,_cap]% %[** .. expand twice, once to get the braces, again to do the Multics lookup **]% %[*expandv,&_getimgdiv_result,_getimgdiv_result]% %[*expandv,&_getimgdiv_result,_getimgdiv_result]% %[_getimgdiv_result]% END
Programming Observations
Notice the use of the *format builtin to assemble the output into _getimgdiv_result. The macro determines which format string _fmtstring to use, depending on whether no "path" file can be found, or only a 1x version, or only a 2x version, or both versions. It then sets up other variables depending on whether the "target", "class" and "caption" variables are supplied. There are at least 16 different cases that have to be checked.
Quote marks to be emitted as output have to be preceded by an escape character.
A few consequences of the sparsity of the HTMX language:
-
Because the *if builtin has no "else" clause, one has to write constructions like
%[*if,eq,var,="constant",...done if true
%[*if,ne,var,="constant",...done if false -
Because htmx has no inline bracketing construct, one has to write sequences like
%[*if,eq,var,="constant",...first thing to do
%[*if,eq,var,="constant",...second thing to do
...
or else invoke the bracketed sequence by putting it in its own *block and then invoke the block with expand or callv. -
Because there is no easy way to write "and" expressions, one has to write constructions like
%[*if,eq,_gifsizer,="",*if,ne,_gifsizer2x,="",*quotient,&_width,_width2x,=2]%
Other Image Macros
The "tgtpath" parameter to getimgdiv, if supplied, generates a link to some other web page. Sometimes you would like to generate a link to a larger version of the same photo. The getimgpopdiv and getfancybox_li macros support this usage: they require additional HTMX include files and JavaScript code to generate HTML code to display the large image. (Fancybox is a jQuery plugin described in http://fancyapps.com/fancybox/3/.) Many of my sites use a standard 75x75 pixel thumbnail, so I have also created getimgdiv75, getimgtag75, and getimgpopdiv75. The lightbox_li macro is like getfancybox_li but does not handle the -2x case for the popup picture, and assumes the image directory is "mulimg".