18 Sep 2020

Macros in expandfile

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.

MacroargsFunction
getimgtagpath,alttag,titletagOutput a responsive IMG tag
getimgdivpath,target,alttag,titletag,class,captionOutput a DIV wrapping a responsive IMG tag
getimgpopdivpath,target,alttag,titletag,class,thumbcaption,popupcaptionOutput a DIV wrapping a responsive IMG tag with a popup image
getimgtag75path,alttag,titletagoutput an IMG tag of size 75x75
getimgdiv75path,target,alttag,titletag,class,captionoutput a DIV wrapping an IMG tag of size 75x75 with a caption
getimgpopdiv75path,target,alttag,titletag,class,thumbcaption,popupcaptionoutput a DIV wrapping an IMG tag of size 75x75 with a caption and a popup image
getfancybox_lidividtag,thumbnailsdir,jpgfile,caption[,imgdir]output an LI tag with a responsive IMG for use with fancybox jquery plugin
lightbox_lidividtag,thumbnailsdir,jpgfile,captionoutput an LI tag for use with fancybox jquery plugin
gettwodigitnumberreturn the conversion of numeric argument to two digits
getformattedsecsnumsecsreturn the conversion of argument in seconds to formatted time hh:mm:ss
getcomma3numberreturn the conversion of numeric argument formatted with commas every 3 digits
getapplemenutextstringreturn text string wrapped in a SPAN with class macmenu
imgtagpath,alttag,titletagset imgtag_result to an IMG tag
imgdivpath,target,alttag,titletag,class,captionset imgtag_result to a DIV tag with caption
imgpopdivpath,target,alttag,titletag,class,thumbcaption,popupcaptionreturn a DIV tag with caption
twodigitnumberset x0 to conversion of integer argument to 2 digits
setscalenumberset x0 to conversion of integer argument to scaled number of bytes
dumpsql dump the values bound by *sqlloop statement to an html formatted dump
javatagclassfile,name,width,height,title,sorrymsg,standbymsg,paramblocknameoutput an APPLET tag
headinginitset up heading macros
heading2titleincrement level2 and output an H2 tag and add a line to toc
heading3titleincrement level3 and output an H3 tag and add a line to toc
heading4titleincrement level4 and output an H4 tag and add a line to toc
wrapvariable,prefix,suffixif variable is nonempty, return its value surrounded by prefix and suffix
firstnonemptyvar1,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.

example.htmx
%[*include,=htmxlib.htmi]% %[*set,&x,=12345678]% %[*callv,setscale,x]% The file size is %[x0]% %[y0]%.
output
The file size is 12 MB.

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 that fill in the image dimensions when generating HTML.

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

example.htmx
%[*include,=htmxlib.htmi]% %[*callv,getimgdiv,="thumb-t1.jpg",="target",="alt tag",="title tag",="myclass",="my caption"]%
output
<div class="myclass" style="width: 949px"> <a href="target"> <img src="thumb-t1.jpg" width="949" height="648" border="0" alt="alt tag" title="title tag"> </a> <p class="caption">my caption</p> </div>

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 stretch or shrink the image to fit into the specified size. When modern browsers do this, they use information about your screen's actual 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.

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="" width="75" height="75">

<img src="thumb-t1-2x.jpg"
alt="" 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

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 thumb-t1.jpg, it will check for the existence of thumb-t1-2x.jpg. To do this, getimgdiv uses the *shell builtin to invoke a helper program called gifsize2. 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 thumb-t1.jpg
  75 75 thumb-t1.jpg
  $ gifsize2 thumb-t1.jpg 2x
  150 150 thumb-t1-2x.jpg
  $ gifsize2 thumb-t1.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 WIDTH= that matches the 1x width, containing an IMG tag with the same WIDTH= and a SRCSET attribute pointing to both file versions.

example.htmx
%[*callv,getimgdiv,="thumb-t1.jpg",="targetpath",="alt tag",="title tag",="myclass",="my caption"]%
output
<div class="myclass" style="width: 75px"> <a href="targetpath"> <img src="thumb-t1.jpg" width="75" height="75" border="0" alt="alt tag" title="title tag"   srcset="thumb-t1.jpg 1x, thumb-t1-2x.jpg 2x"> </a> <p class="caption">my caption</p> </div>

getimgdiv handles several alternate cases:

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: