10

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.

8
  • \marginnote has an optional argument for vertical offset: \marginnote[<left>]{<right>}[<voffset>]
    – cgnieder
    Dec 4, 2014 at 11:18
  • Thanks. I have this all over the place so it has to be automatic, just like marginpar. Dec 4, 2014 at 11:21
  • 1
    Do you want it to expand downward or in both directions? BTW, a MWE would be nice. Dec 4, 2014 at 14:49
  • Can you add an example of what you're trying to achieve?
    – egreg
    Dec 4, 2014 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. Dec 5, 2014 at 20:57

1 Answer 1

18

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

6
  • 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.) Dec 8, 2014 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. Dec 18, 2014 at 22:51
  • Okay, it's done. I had an epiphany and had to try it out. Dec 20, 2014 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)! Jan 29, 2019 at 19:30
  • 1
    Did you ever get around to revisiting this code, @JohnKormylo? For documents with many marginnotes, your code above enters an infinite loop for me, then runs out of memory. However, I have no idea how to debug it. May 5, 2022 at 18:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .