6

Background

Creating a configurable cover letter, which has a two-column layout. The first column is for address information, the second column is for the content.

Problem

I'd like to control the width of the first column to differ from the width of the second column. Currently, the widths are the same, as this screen capture shows:

cover letter columns

The desired output resembles (well, with "elit" word wrapping properly, but the idea is close enough):

cover letter with column width

Various attempts to make this layout work have failed:

  • Extreme tables -- resulted in overflow error
  • Natural tables -- overflow error
  • \startcolumns
  • \defineparagraphs -- weird behaviour
  • \startboxedcolumns -- affects entire document
  • \startpagecolumns -- inapplicable because the columns start after the header
  • \startmixedcolumns -- cannot set width
  • \startcolumnsets -- rearranges content, puts in a new page regardless of \pagebreak setting
  • \usemodule[newcolumnsets] -- the "Sincerely," line jumps to the bottom of column
  • Custom \hbox and \vtop -- couldn't reinstate paragraph breaks
  • Hanging \frameds -- couldn't reinstate paragraph breaks

Code

In a nutshell, the closest code that almost works, as shown in the screen capture, is:

\definestartstop[letter]

\setupstartstop[contact][
  before={%
    \startmixedcolumns[
      n=2
    ]\bgroup%
  },
  after={\column},
]

\setupstartstop[letter][
  after={\egroup\stopmixedcolumns},
]

However, there's no width key to tweak the first column's width.

Markdown

The cover letter stems from a Markdown document:

::: header
::: logo
![{{employer.company.name}}](logo)
:::

[{{employee.name}}]{.applicant}

[{{employee.role}}]{.role}
:::

::: contact

::: address
{{employee.address.line.1}}

{{employee.address.line.2}}

{{employee.address.line.3}}
:::

[{{employee.contact.phone}}]{.phone}

[{{employee.contact.email}}]{.email}

[{{employee.portfolio.url}}]{.portfolio}
:::

::: letter
::: opening
To whom it may concern,
:::

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

::: closing
Sincerely,

::: signature
![{{employee.name}}](signature)
:::

{{employee.name}}
:::

XHTML

The Markdown is converted to XHTML:

<body>
<div class="header"><div class="logo">
<p><img alt="Henry Baskerville" src="logo.svg"/></p>
</div>
<p><span class="applicant">Sherlock Holmes</span></p>
<p><span class="role">Private Investigator</span></p>
</div><div class="contact"><div class="address">
<p>221B Baker Street</p>
<p>Marlyebone, London</p>
<p>NW1 6XE</p>
</div>
<p><span class="phone">020 7224 3688</span></p>
<p><span class="email">[email protected]</span></p>
<p><span class="portfolio">https://www.sherlock-holmes.co.uk</span></p>
</div><div class="letter"><div class="opening">
<p>To whom it may concern,</p>
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
<div class="closing">
<p>Sincerely,</p>
<div class="signature">
<p><img alt="Sherlock Holmes" src="signature.png"/></p>
</div>
<p>Sherlock Holmes</p>
</div></div>
</body>

There's a fair amount of infrastructure that converts the XML to ConTeXt macros.

Minimal example

A fairly trimmed down example of the code:

\definestartstop[header]
\definestartstop[letter]
\definestartstop[opening]
\definestartstop[closing]
\definestartstop[signature]
\definestartstop[contact]
\definestartstop[address]
\definestartstop[applicant]
\definestartstop[role]

\setupstartstop[contact][
  before={%
    \startmixedcolumns[
      n=2,
      separator=rule,
    ]\bgroup%
  },
  after={\column},
]

\setupstartstop[letter][
  after={\egroup\stopmixedcolumns},
]

%%% The content between \starttext and \stoptext is immutable,
%%% because it is sourced from an XML document.

\starttext

\startheader
\startlogo
\externalfigure[logo.svg]
\stoplogo

\startapplicant Sherlock Holmes \stopapplicant

\startrole Private Investigator \stoprole
\stopheader

\blackrule[width=\textwidth]

\startcontact
  \startaddress
  221B Baker Street \par
  Marlyebone, London \par
  NW1 6XE \par
  \stopaddress

  020 7224 3688 \par
  [email protected] \par
  https://www.sherlock-holmes.co.uk \par
\stopcontact

\startletter
\startopening
To whom it may concern,
\stopopening

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

\startclosing
Sincerely,

\startsignature
  \externalfigure[signature.png]
\stopsignature

Sherlock Holmes
\stopclosing

\stoptext

Question

What approach would you take to set up unequal columns widths that start below existing content?

For example, should I just move the logo and the name + role into the actual header? Or is there another approach that'll work?

11
  • 1
    With LuaLaTeX, you could use the following: tex.stackexchange.com/a/695271/270600. It should be fairly easy to convert that to ConTeXt, see the following for a somewhat-related example: tex.stackexchange.com/a/644613/270600. I'll try and post an answer tomorrow if Wolfgang doesn't come along with a “this is already built-in to ConTeXt” answer. Commented yesterday
  • Thanks Max. I'm going to see if putting the header information into the actual page header works. Maybe that'll open up some more columnar possibilities that respect width settings. Commented yesterday
  • 'Extreme tables'? Commented yesterday
  • wiki.contextgarden.net/index.php?title=Tables/… Commented yesterday
  • I do not understand what you want. In your black image, is all the stuff above the horizontal line a logo? Should that have full width? And below, you want the left part to be narrower than the right? Will the letter ever run over more than one page? If so, what should the second page look like? Just the right, wider(?), column? Commented 19 hours ago

2 Answers 2

2

I probably miss some point, sorry. This is too long for a comment, but I will delete in case I misunderstood.

Can't you just use a tabulate for this? Small example with output:

\setuplayout[tight]

\starttext

\startframed
  [width=broad,
   height=5\lineheight,
   align={middle,lohi}]
  !!!!!!!!!
\stopframed

\starttabulate[|p(7cm)|p|]
\NC
  221B Baker Street\par
  Marlyebone, London\par
  NW1 6XE\par
  020 7224 3688\par
  [email protected]\par
  https://www.sherlock-holmes.co.uk\par
\NC
  \dorecurse {12}{\samplefile {ward}\par}
\NC
\NR
\stoptabulate

\samplefile {ward}

\stoptext

Output:

columns of different width

Using columnsets

You can try out with two mvls in a columnset, but I don't know how that fits with the rest of your document.

\setupinterlinespace
  [top=height]

\setuplayout
  [grid=yes,
   topspace=2cm,
   header=0cm,
   backspace=2cm,
   width=175mm,
   location=middle,
   lines=49]

\showgrid

\definecolumnset
  [example]
  [n=2,
   distance=5mm]

\setupcolumnset[example:1][width=70mm]
\setupcolumnset[example:2][width=100mm]
\setupcolumnset[example:3][width=70mm]
\setupcolumnset[example:4][width=100mm]

\definesubcolumnset[example][left][1,3]
\definesubcolumnset[example][main][2,4]

\setuppagenumbering
  [alternative=singlesided,
   state=stop]

\starttext

\startcolumnset[example]

\startcolumnsetsheet[1]

\startsetcolumnset[c=1,r=1,nbottom=1]

\startframed
  [width=\columnsethspan{1}{2},
   height=3\lineheight,
   align={middle,lohi},
   background=color,
   backgroundcolor=yellow]
  !!!!!!
\stopframed

\stopsetcolumnset

\stopcolumnsetsheet

\startsubcolumnset[left]
  221B Baker Street\par
  Marlyebone, London\par
  NW1 6XE\par
  020 7224 3688\par
  [email protected]\par
  https://www.sherlock-holmes.co.uk\par
\stopsubcolumnset

\startsubcolumnset[main]

\dorecurse{5}{\samplefile {knuth}\par}

\stopsubcolumnset

\stopcolumnset

\stoptext

using columnsets

0
2

We can combine layers with a ConTeXt version of my paragraph width adjusting answer:

\newdimen\mycolumnwidth

\startluacode
    -- Save some LuaTeX internal node functions
    local copy_list = node.copylist
    local find_attribute = node.findattribute
    local get_attribute = node.getattribute
    local has_attribute = node.hasattribute
    local set_attribute = node.setattribute
    local slide = node.slide
    local traverse = node.traverse


    -- Define some module-global variables
    local attr = attributes.public("mark_paragraph")
    local count = 1
    local saved = node.new("glue")
    local mycolumnwidth = 0


    -- Mark each node in a paragraph and save the pre-broken tagged contents
    function userdata.save_paragraph(head)
        if head.id ~= node.id("par") then return head end

        for n in traverse(head) do
            set_attribute(n, attr, count)
            count = count + 1
        end

        slide(saved).next = copy_list(head)
        mycolumnwidth = tex.dimen.mycolumnwidth

        return head
    end

    nodes.tasks.appendaction("processors", "after", "userdata.save_paragraph")


    -- Fix the width of any partial paragraphs carried over from the previous page
    function userdata.fix_paragraph(head)
        -- do nothing if mycolumnwidth has not been changed
        if mycolumnwidth == tex.dimen.mycolumnwidth then return head end

        local first_index, first_n, first_list
        local last_index, last_n, last_list

        for list in traverse(tex.lists.contributehead) do
            if not list.list then goto continue end
            local inner = list.list

            if not first_index then
                first_index = find_attribute(inner, attr)
                first_list = list
            end

            local prev_last_index
            local m = inner
            repeat
                if (last_index or 0) < (prev_last_index or 0) then
                    last_index = prev_last_index
                end
                prev_last_index, m = find_attribute(m.next, attr)
            until (not prev_last_index) or (not m.next)
            last_list = list

            ::continue::
        end

        if not first_index or not last_index then return head end

        node.slide(saved)
        for n in traverse(saved) do
            if not first_n and
            (get_attribute(n, attr) or 0) >= first_index
            then
                first_n = n
            end

            if has_attribute(n, attr, last_index) then
                last_n = n
                break
            end
        end

        if not first_n or not last_n then return head end

        for n in traverse(first_n.next) do
            if n == last_n then break end
            if n.id == node.id("par") then
                last_n = n.prev
                break
            end
        end

        first_n.prev.next = nil
        node.flushlist(saved)
        saved = node.new("glue")

        local new = copy_list(first_n, last_n.next)

        if new.id ~= node.id("par") then
            local par = node.new("par")
            local indent = node.new("glue", "indentskip")
            par.next = indent
            indent.next = new
            new = par
        end

        tex.preparelinebreak(new)
        local broken = tex.linebreak(new, { hsize = tex.dimen.mycolumnwidth })

        first_list.prev.next = broken
        slide(broken).next = last_list.next

        last_list.next = nil
        node.flushlist(first_list)

        return head
    end

    nodes.tasks.appendaction("shipouts", "after", "userdata.fix_paragraph")
\stopluacode

\mycolumnwidth=\textwidth

\appendtoks
    \global\mycolumnwidth=\GetColumnWidth{\numexpr\realpageno+1\relax}%
\to\everybeforeoutput
\appendtoks
    \setuplayout[width=\mycolumnwidth, backspace=\GetBackspace{\realpageno}]%
\to\everyafteroutput

%%% Demonstration
\setuppapersize[A6, landscape][A3]

\setuppaper[nx=2, ny=4, offset=-1.5pt]
\setuparranging[XY]

\definepalet[layout][page=black]
\showframe[page]
\setupbackgrounds[page][rulethickness=2pt]

\def\GetColumnWidth#1{
    \ifodd#1
        150pt
    \else
        300pt
    \fi
}
\def\GetBackspace#1{
    \ifodd#1
        200pt
    \else
        50pt
    \fi
}
\setuplayout[width=\GetColumnWidth{1}, backspace=\GetBackspace{1}]

\setupindenting[yes, big]
\setupalign[hz, hanging, stretch, tolerant]
\setupheader[style=bold]

\startsetups[*default]
    \directsetup{*reset}
    \clubpenalty=0
    \widowpenalty=0
\stopsetups

\definelayer[mylayer][
    x=50pt,
    width=150pt,
    y=\dimexpr\topspace + \headerheight + \headerdistance\relax,
    height=\textheight,
]

\setupbodyfont[modern]
\savebox{mybox}{mybox}{
    \startframedtext[
        radius=10pt,
        corner=round,
        width=125pt,
        height=\textheight,
        offset=10pt,
    ]
        This is an odd page.
    \stopframedtext
}

\startsetups[mylayer]
    \setlayer[mylayer]{
        \ifodd\realpageno
            \foundbox{mybox}{mybox}%
        \fi
    }
\stopsetups

\setupbackgrounds[page][setups=mylayer, background=mylayer]

\starttext
    \dorecurse{7}{\recurselevel\ \samplefile{knuth}\par}
\stoptext

output

3
  • Your answers never cease to amaze, Max. Thank you. Do you see a way to pop the contents the start/stop header (::: header -> \startheader ... \stopheader) into the \setupheadertext macro? I tried \definebuffer and other machinery. A sub-goal for this is a blog post showing how simple ConTeXt can be to configure; is dropping into Lua really needed to set the width of a column? Commented 15 hours ago
  • @DaveJarvis “Do you see a way to pop the contents the start/stop header (::: header -> \startheader ... \stopheader) into the \setupheadertext macro?” I'm able to put arbitrary content into a header, so I'm not really sure what you mean? But adding a \framed often helps with things: \setupheadertexts[{\framed[width=broad]{\externalfigure[sys:///cow.pdf]}}]. You might find it easier to work with layers though; I'm pretty sure that it's always possible to replace headers/footers with layers (but not the other way around). Commented 14 hours ago
  • 1
    @DaveJarvis “is dropping into Lua really needed to set the width of a column?” To set the width of a column for any future paragraphs: no. To change the width of a column in the middle of a paragraph, without needing to manually adjust \parshape: yes. But you might be able to use side floats to accomplish something similar; §5 of “It's in the details” has a few examples. Commented 14 hours ago

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.