MERGE: One Letter Per Row, Without Uploading the List
A merge list is a membership roll, a donor list, a ward list, a list of people being let go. The ordinary way to get letters out of one is to upload it to a website, and that is the part that should never have been ordinary. This does the merge in the tab: a markdown template with {{field}} in it, a CSV read properly, a count of every hole before anything is produced, and four outputs when there are none left.
1. The letter
Markdown for the shape of it, {{field}} for anything that changes from one letter to the next. A line break is a line break, so an address block stays an address block.
Headings with #, ## and ###. Bold with **stars**, italic with *one*. Bullets with a dash, a numbered list with 1. And one conditional: a line that starts {{#if field}} and ends {{/if}} is dropped when that field is empty for that row. There is no else, no nesting and no loop, on purpose.
2. The list
A CSV whose first line is the header row. Quoted fields, commas inside them, line breaks inside them and a byte order mark on the front are all read properly, because that is what a spreadsheet exports.
Drop a CSV here, or
3. The check Not ready
Before anything is produced: which placeholders have no column, which columns the letter never uses, and which rows would leave a hole in a letter.
4. Read one
5. The address block
Labels and envelopes are printed from this, not from the letter. Same placeholders, same conditional, no markdown: one line here is one line on the label.
6. Paper
The label sheet
Set it to the sheet you actually have. Every measurement is in millimetres, and the label size follows from them.
7. Produce
Nothing here will run while the check above is unhappy.
One PDF
Every letter in one file, each starting on its own page. For the printer.
A zip of text files
One .txt per row, named after the row. For sending, or for feeding to something else.
Label sheets
The address block on the grid set above, sheet after sheet.
Envelopes
One envelope per row at the size chosen above, with your return address in the corner.
What this is, and what it is not
It will not print {{name}} into somebody's letter. Every merge tool will happily post a letter beginning "Dear {{name}}," because the column was called something else. Before it will produce anything, this one lists the placeholders that have no column and the rows whose value for a needed field is empty, and stays refused until both are nil. An empty value inside an {{#if}} line does not count, because dropping the line is what that construct is for.
The list is held in the tab and nowhere else. The template, the address block and the paper settings are saved on this device so they are there tomorrow. The list is not saved anywhere: it is read into memory, used, and gone when the tab closes or when you press Forget the list. Nothing is uploaded, and the page works with the network off.
One conditional, and that is the whole template language. A line that opens with {{#if field}} and closes with {{/if}} on the same line is dropped when that field is empty. There is no else, no nesting, no loops and no expressions. Anything more and a letter becomes a program somebody has to debug at four o'clock on a Friday; anything less and the second address line prints as a blank line in four hundred envelopes.
The CSV reader is the pedantic kind. A byte order mark, CRLF, a bare carriage return from an old Mac export, a field wrapped in quotes, a comma inside that field, a doubled quote meaning a literal one, and a line break inside a field are all handled. The header row names the fields, matched to placeholders without regard to case or extra spaces, so a column called First Name answers {{first name}}.
The PDFs are set in a Latin alphabet, and say so when they cannot be. Letters, labels and envelopes are drawn with the fonts every PDF reader already has, which between them cover Western European languages and no more. A name in Greek, Cyrillic, Arabic, Hebrew or any CJK script cannot be drawn by them and comes out as a question mark; when that happens the page counts the characters and tells you, rather than letting you find out from the envelope. The text file per row is UTF-8 and carries every name exactly, so that is the output to use for a list those fonts cannot set.
Check one before you post four hundred. The preview shows a real row, not a mock-up, and the row picker steps through the lot. Labels and envelopes are laid out from measurements you give, which means they fit the sheet in your drawer rather than a sheet somebody assumed: print one on plain paper and hold it against the real thing before committing a box of labels.
Questions people ask
Does the list get uploaded?
No. The file is read by this tab with the browser's own file reader, the rows are held in memory, and the letters are written here. There is no server side to this page, and a Content Security Policy on this site forbids it talking to any other origin. Turn the network off and it still works.
What happens if a row has an empty cell?
If the letter uses that field on an ordinary line, the check lists the row and refuses to produce anything until it is filled or the line is made conditional. If the field is used only inside an {{#if}} line, the line is dropped for that row and nothing else changes.
Which bits of markdown does it understand?
Headings with one to three #, paragraphs, bullet lists with - or *, numbered lists with 1., bold with **stars** and italic with *one star* or _underscores_. Every line break in the template is a line break in the letter, which is not standard markdown but is what an address block needs.
Can I use it for labels on a sheet I already have?
Yes, that is why the grid is a set of numbers rather than a list of brand names. Measure the sheet: how many across, how many down, the margin at the top and at the side, and the gap between labels. The label size follows from those. Print one sheet on plain paper and hold it up to the light against a real one.
Why only one kind of conditional?
Because the second one is never the last. A merge template is read by whoever inherits the letter, often a year later and often not a programmer, and a language with nesting and expressions in it stops being readable at about the third feature. A line that is there when the field has something in it covers the case that actually comes up, which is the second address line.
My list has names that are not in the Latin alphabet. What happens?
In the three PDF outputs, each character those fonts cannot draw comes out as a question mark, and the page says how many did after it writes the file. That is a limit of the fourteen fonts built into the PDF format, not a decision made here. The zip of text files is UTF-8 and carries every name exactly, so for a list in Greek, Cyrillic, Arabic, Hebrew or a CJK script, produce the text files and set them in something that has the fonts.
Where did the letter go after I closed the tab?
The template, address block and settings are still there, saved on this device. The list is not, by design. Keep the CSV where you got it and open it again.