6

If multiple \marginnote commands are triggered on the same line, by default they are typeset on top of one another, making them unreadable. I would like them to stack vertically, which is the default behaviour of \marginpar. It doesn't matter that some of them will therefore not be rendered immediately adjacent to the line in which they were generated.

I need this to work both in math environments and non-math environments, so I can't use \marginpar itself. And I have this all over the place, so it has to be automatic like marginpar; I can't manually adjust the vertical spacing of each one.

  • \marginnote has an optional argument for vertical offset: \marginnote[<left>]{<right>}[<voffset>] – clemens Dec 4 '14 at 11:18
  • Thanks. I have this all over the place so it has to be automatic, just like marginpar. – Jamie Vicary Dec 4 '14 at 11:21
  • 1
    Do you want it to expand downward or in both directions? BTW, a MWE would be nice. – John Kormylo Dec 4 '14 at 14:49
  • Can you add an example of what you're trying to achieve? – egreg Dec 4 '14 at 16:06
  • I think can do this, but it will take some time. In addition to not overlapping, I assume you don't want it going off the bottom of the page either. – John Kormylo Dec 5 '14 at 20:57
14

report

It takes two runs for the margin notes to show up. I put the content as well as the location into the aux file so that I didn't have to synch two different sources.

I used \marginparpush to separate consecutive margin notes vertically. I added \marginfillstyle to set the font etc.

\documentclass{article}
\usepackage{everypage}
\usepackage{amsmath}

\newcounter{abspage}
\AddEverypageHook{\marginfillpage}

\pdfpageheight\paperheight
\pdfpagewidth\paperwidth

% user modifiable formatting
\newcommand{\marginfillstyle}{\raggedright\normalsize\normalfont\hspace{0pt}}

\makeatletter
\newcount\MF@index% global variables
\newcount\MF@total
\newlength{\MF@x}% marginpar offset
\newlength{\MF@y}% pagehook cursor location
\newlength{\MF@top}% marginpar area
\newlength{\MF@bottom}
\newlength{\MF@width}% \marginparwidth might get clobbered
\newlength{\MF@push}% \marginparpush might get clobbered
\newcount\MF@min% needed for nested loops
\newcount\MF@max
\newlength{\MF@adjust}
\newif\ifMF@repeat
\newif\ifMF@main

\newcommand{\marginfill}[1]% #1 = text for margin note
{\pdfsavepos
  \protected@write\@auxout{\let\theabspage=\relax}{\string\newmarginfill{\theabspage}%
    {\noexpand\number\pdflastypos}{#1}}%
}%

\newcommand{\newmarginfill}[3]% #1 = abspage, #2 = location, #3 = text
{\global\advance\MF@total by \@ne
  \expandafter\gdef\csname MF@page\the\MF@total\endcsname{#1}%
  \expandafter\gdef\csname MF@location\the\MF@total\endcsname{#2}%
  \expandafter\gdef\csname MF@note\the\MF@total\endcsname{#3}%
}%

\newcommand{\marginfillpage}% cursor 1in from top left corner
{\stepcounter{abspage}%
\ifodd\c@page
  \setlength{\MF@x}{\oddsidemargin}%
  \addtolength{\MF@x}{\textwidth}%
  \addtolength{\MF@x}{\marginparsep}%
\else
  \setlength{\MF@x}{\evensidemargin}%
  \addtolength{\MF@x}{-\marginparsep}%
  \addtolength{\MF@x}{-\marginparwidth}%
\fi
\setlength{\MF@y}{\paperheight}%
\addtolength{\MF@y}{-1in}%
\setlength{\MF@top}{\MF@y}%
\addtolength{\MF@top}{-\topmargin}%
\addtolength{\MF@top}{-\headheight}%
\addtolength{\MF@top}{-\headsep}%
\setlength{\MF@bottom}{\MF@top}%
\addtolength{\MF@bottom}{-\textheight}%
\setlength{\MF@width}{\marginparwidth}%
\setlength{\MF@push}{\marginparpush}%
% This algorithm overlays a lot of dimen registers, depending on how many notes are on the page.
% These registers will be restored by the \endgroup, but many will not be available until then.
% The \MF@...  dimens defined above should be safe.
\begingroup
\countdef\index=1
\index=0
\MF@repeattrue
\loop% locate all notes on this page
  \ifnum\MF@index<\MF@total
    \global\advance\MF@index by \@ne
    \edef\temp{\the\MF@index}%
    \@ifundefined{MF@page\temp}{}{%
    \ifnum\value{abspage}=\csname MF@page\temp\endcsname\relax
      \dimendef\yloc=\index
      \yloc=\csname MF@location\temp\endcsname sp\relax
      \savebox{\index}{\parbox[c]{\MF@width}%
        {\marginfillstyle \csname MF@note\temp\endcsname}}%
      \advance\index by \@ne
    \else
      \MF@repeatfalse
      \global\advance\MF@index by \m@ne
    \fi}%
  \else\MF@repeatfalse
  \fi
\ifMF@repeat \repeat
% are there any notes this page?
\ifnum\index>0 \MF@process\fi
\endgroup}

%***** moduals used by \marginfillpage *****

\newcommand{\MF@process}%
{\countdef\total=2
\countdef\min=3
\countdef\max=4
\total=\index
%create additional local length variables
\dimendef\tempdima=\index
\advance\index by \@ne
\dimendef\tempdimb=\index
\advance\index by \@ne
\dimendef\tempdimc=\index
% resolve overlaps
\MF@freespace{\tempdimb}%
\ifdim\tempdimb<1pt \MF@cram{\tempdimb}%
\else \MF@findsolution
\fi
% output notes to page
\index=0
\loop
  \dimendef\yloc=\index
  \MF@adjust=\yloc% in case \rlap or \raisebox clobbers \yloc
  \advance\MF@adjust by -\MF@y
  \rlap{\hspace{\MF@x}\raisebox{\MF@adjust}[0pt][0pt]{\usebox{\index}}}%
  \advance\index by \@ne
\ifnum\index<\total \repeat}

\newcommand{\MF@freespace}[1]% #1 = dimen to return total free space
{\let\freespace=#1%
\freespace=\MF@top
\advance \freespace by -\MF@bottom
\index=0
\loop% check for no solution
  \settoheight{\tempdima}{\usebox{\index}}%
  \advance\freespace by -\tempdima
  \settodepth{\tempdima}{\usebox{\index}}%
  \advance\freespace by -\tempdima
  \advance\freespace by -\MF@push
  \advance\index by \@ne
\ifnum\index<\total\relax \repeat
\advance\freespace by \MF@push}

\newcommand{\MF@cram}[1]% #1 = free space
{\let\top=#1%
\top=-0.5\top% center excess
\advance\top by \MF@top
\index=0
\loop% overlap top and bottom
  \settoheight{\tempdima}{\usebox{\index}}%
  \advance\top by -\tempdima
  \dimendef\yloc=\index
  \yloc=\top
  \settodepth{\tempdima}{\usebox{\index}}%
  \advance\top by -\tempdima
  \advance\top by -\MF@push
  \advance\index by \@ne
\ifnum\index<\total\relax \repeat}

\newcommand{\MF@findsolution}% main loop
{\min=0% start of middle
\max=\total% end of middle+1
\loop
  \MF@mainfalse
  \MF@addtotop
  \MF@addtobottom
  \ifMF@main\relax\else% wait until top and bottom done
    \MF@min=\min
    \MF@max=\max
    \advance\MF@max by \m@ne
    \ifnum\MF@min<\MF@max \MF@locate\fi% find overlapping notes in middle
    \ifnum\MF@min<\MF@max
      \MF@maintrue
      \MF@apply% cannot use nested loop
      \ifnum\MF@min<\MF@max \MF@apply\fi
      \ifnum\MF@min<\MF@max \MF@apply\fi
      \ifnum\MF@min<\MF@max \MF@apply\fi
      \ifnum\MF@min<\MF@max \MF@apply\fi
      \ifnum\MF@min<\MF@max \MF@apply\fi
      \ifnum\MF@min<\MF@max
        \errormessage{Exceeded \string\marginfill algorithm capacity.}
      \fi
      \MF@apply
    \fi
  \fi
\ifMF@main \repeat}

\newcommand{\MF@addtotop}% resets \min and \ifMF@main
{\let\top=\tempdimb
\ifnum\min=0 \top=\MF@top
\else
  \index=\min
  \advance\index by \m@ne
  \dimendef\yloc=\index
  \top=\yloc
  \settodepth{\tempdima}{\usebox{\index}}%
  \advance\top by -\tempdima
  \advance\top by -\MF@push
\fi
\settoheight{\tempdima}{\usebox{\min}}%
\advance\top by -\tempdima
\dimendef\yloc=\min
\ifdim\top<\yloc
  \yloc=\top
  \advance\min by \@ne
  \MF@maintrue
\fi}

\newcommand{\MF@addtobottom}% resets \max and \ifMF@main
{\let\bottom=\tempdimb
\ifnum\max=\total \bottom=\MF@bottom
\else
  \dimendef\yloc=\max
  \bottom=\yloc
  \settoheight{\tempdima}{\usebox{\max}}%
  \advance\bottom by \tempdima
  \advance\bottom by \MF@push
\fi
\index=\max
\advance\index by \m@ne
\settodepth{\tempdima}{\usebox{\index}}%
\advance\bottom by \tempdima
\dimendef\yloc=\index
\ifdim\bottom>\yloc
  \yloc=\bottom
  \max=\index
  \MF@maintrue
\fi}

\newcommand{\MF@locate}% returns \MF@min, \MF@max and \MF@adjust
{\begingroup% needed for nested loop
\let\top=\tempdimb
\let\test=\tempdimc
\let\size=\MF@adjust
\dimendef\yloc=\min
\top=\yloc
\settodepth{\tempdima}{\usebox{\min}}%
\advance\top by -\tempdima
\advance\top by -\MF@push
%
\size=0pt
\index=\min
\loop% find largest overlap
  \advance\index by \@ne
  \settoheight{\tempdima}{\usebox{\index}}%
  \advance \top by -\tempdima
  \dimendef\yloc=\index
  \test=\yloc
  \advance\test by -\top
  \ifdim\test>\size \size=\test
    \MF@min=\index
  \fi
  \top=\yloc
  \settodepth{\tempdima}{\usebox{\index}}%
  \advance\top by -\tempdima
  \advance\top by -\MF@push
\ifnum\index<\MF@max \repeat% \MF@max = \max-1
\MF@max=\MF@min
\ifdim\size>1pt
  \advance\MF@min by \m@ne
  \MF@expand% find all contiguous overlaps
  \MF@average% returns \MF@adjust
\fi
\global\MF@min=\MF@min
\global\MF@max=\MF@max
\global\MF@adjust=\MF@adjust
\endgroup}

\newcommand{\MF@expand}% locate all contiguous margin notes
{\let\test=\tempdimb
\dimendef\yloc=\MF@min
\test=\yloc
\advance\test by 0.5\size% add correction
\loop
  \MF@repeatfalse
  \ifnum\MF@min > \min
    \settoheight{\tempdima}{\usebox{\MF@min}}%
    \advance\test by \tempdima
    \advance\test by \MF@push
    \advance\MF@min by \m@ne
    \settodepth{\tempdima}{\usebox{\MF@min}}%
    \advance\test by \tempdima
    \dimendef\yloc=\MF@min
    \ifdim\yloc>\test \advance\MF@min by \@ne
    \else\MF@repeattrue
    \fi
  \fi
\ifMF@repeat \repeat
%
\dimendef\yloc=\MF@max
\test=\yloc
\advance\test by -0.5\size% add correction
\loop
  \MF@repeatfalse
  \ifnum\MF@max>\max\relax\else
    \settodepth{\tempdima}{\usebox{\MF@max}}%
    \advance\test by -\tempdima
    \advance\test by -\MF@push
    \advance\MF@max by \@ne
    \settoheight{\tempdima}{\usebox{\MF@max}}%
    \advance\test by -\tempdima
    \dimendef\yloc=\MF@max
    \ifdim\yloc<\test \advance\MF@max by \m@ne
    \else\MF@repeattrue
    \fi
  \fi
\ifMF@repeat \repeat}

\newcommand{\MF@average}% returns \MF@adjust
{\let\offset=\tempdimb
\offset=0pt
\MF@adjust=0pt
\index=\MF@min
\loop
  \settoheight{\tempdima}{\usebox{\index}}%
  \advance\offset by \tempdima
  \advance\MF@adjust by \offset
  \dimendef\yloc=\index
  \advance\MF@adjust by \yloc
  \settodepth{\tempdima}{\usebox{\index}}%
  \advance\offset by \tempdima
  \advance\offset by \MF@push
  \advance\index by \@ne
\ifnum\index>\MF@max\relax\else \repeat
\advance\index by -\MF@min
\divide\MF@adjust by \index}

\newcommand{\MF@apply}% corrects one margin note at a time
{\settoheight{\tempdima}{\usebox{\MF@min}}%
\advance\MF@adjust by -\tempdima
\dimendef\yloc=\MF@min
\yloc=\MF@adjust
\settodepth{\tempdima}{\usebox{\MF@min}}%
\advance\MF@adjust by -\tempdima
\advance\MF@adjust by -\MF@push
\advance\MF@min by \@ne}
\makeatother

\begin{document}
\noindent\rule{\textwidth}{1pt}
First line \marginfill{First note.}%
with not enough room. \marginfill{Second note}

\vfill
Isolated group in display style
\begin{align}
x &= a &\marginfill{Third note is a longer note.}\\
y &= b &\marginfill{Fourth note is a longer note.}
\end{align}

\vfill
Last line \marginfill{Fifth note.}%
with not enough room. \marginfill{Sixth note.}

\noindent\rule{\textwidth}{1pt}
\newpage
First line in text area \marginfill{text area}
\begin{figure}[t]
\marginfill{float}
\centering\rule{.5in}{.5in}
\end{figure}
\end{document}

test

  • Thanks for this nice analysis! I typically have 2 or 3 margin notes declared on the same line, so the unconstrained approach would definitely not be sufficient (and anyway that's what \marginnote allows already.) – Jamie Vicary Dec 8 '14 at 18:04
  • I am still debugging my code, but am very close. However, I have been spending way too much time on this and will not return to the project until after Xmas. – John Kormylo Dec 18 '14 at 22:51
  • Okay, it's done. I had an epiphany and had to try it out. – John Kormylo Dec 20 '14 at 4:59
  • Wow, this looks amazing (+1). Are you aware of any bugs/limitations? If this is reliable it could be a package (and I would use it)! – Circumscribe Jan 29 at 19:30
  • No bugs that I know of, but I keep wanting to redo the whole thing using global macros instead of local registers. – John Kormylo Jan 30 at 14:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.