2

The image below shows the result for the second page of the following code:

enter image description here

\fontfam[Myriad]

\margins/2 a4 (2,6,2,2)cm \mnotesize=4.5cm

\newcount\notenum %\fixmnotes\right \def\note#1{\global\advance\notenum by 1 \fnmark{\the\notenum}% \mnote{\fnmark{\the\notenum}\kern.3em #1}% }

\def\fnmark#1{\leavevmode\raise.7ex\hbox{\setfontsize{mag.6}\setff{+lnum}\setff{-onum}\currvar #1}}

%and use: % %Text\fnmark{69} text. % %If you want to use this in footnotes, try to define:

\def_printfnotemark{\fnmark{_fnotenum}}

\onum\rm

ASDF1234567890\note{asdf}

ASDF\note{asdf} And this note will be above the next one \mnoteskip -\baselineskip\note{asdf}

\mnoteskip =-\baselineskip ASDF\note{asdf}

\lipsum[1]\note{asdf}

\vfill \eject

ASDF1234567890\note{This is a very long note to check out if we’re going to need more space below.}

ASDF \mnoteskip-2\baselineskip \note{asdf} And this note will be above the next one. \mnoteskip-3\baselineskip \note{asdf}\mnoteskip-4\baselineskip\note{asdf}

ASDF\mnoteskip-5\baselineskip \note{asdf}

\lipsum[1]\note{asdf}

\bye

The worrisome part is that we have to adjust the vertical space manually with \mnoteskip whenever there is more than one note or a long note. Is there a way to set this space according to the space of the previous note?

user574859
  • 867
  • 3
  • 11
  • 2
    If a simple solution of your task existed then it would been implemented in OpTeX. IMHO, this isn't trivial task. What the automaton should do if there are more "notes" material than "normal pages" material in the document? The positions of notes can be saved to the ref file and read them in the next OpTeX run and diagnose if there are overlapping notes and then decide, what to do with overlapping notes. But how to decide? We need a reasonable rule. – wipet Jun 25 '23 at 04:44
  • I was thinking something like the latex package scrlayer-notecolumn (https://tex.stackexchange.com/questions/295864/tufte-alike-design-with-sidenotes-crossing-pagebreaks-or-tufte-made-with-koma), but now I see it's very difficult to implement anyway. – user574859 Jun 25 '23 at 10:48

1 Answers1

6

If we add the sidenotes at shipout time using Lua, we can make sure that nothing collides. With this mechanism, we only need a single pass, and we don't need to write to any .aux or .ref files.

sidenote-test.tex

\fontfam[lm]

\margins/2 letter (1,3,2,2)in \parindent=0pt \parskip=1ex

\addto_begoutput{% \ifodd\pageno% \sidenote_side\right% \else% \sidenote_side\left% \fi% }

\load[sidenote]

\lorem[1.]\sidenote{\lorem[1.]}

\lorem[2.]\sidenote{\lorem[2.]}

\def_par{\let_par=\par} \lorem[3]\sidenote{\lorem[3.]}

\lorem[4.]\sidenote{\lorem[4.]}

\lorem[5.]\sidenote{\lorem[5.]} \lorem[6.]\sidenote{\lorem[6.]} \lorem[7.]\sidenote{\lorem[7.]}

\vfill\eject

\sidenote_number=199 \def\sidenote_makemark{% \leavevmode% \raise 0.7ex% \hbox{\typosize[8/8]\romannumeral\sidenote_number}% }

\def\sidenote_makeleft#1{% \hsize=2in\relax% \leftskip=0pt plus 1fill\relax% \rightskip=1in\relax% \it% \sidenote_makemark% #1% \vskip 1cm\relax% }

\lorem[1.]\sidenote{\lorem[1.]}

\lorem[2.]\sidenote{\lorem[2.]}

\def_par{\let_par=\par} \lorem[3]\sidenote{\lorem[3.]}

\lorem[4.]\sidenote{\lorem[4.]}

\lorem[5.]\sidenote{\lorem[5.]} \lorem[6.]\sidenote{\lorem[6.]} \lorem[7.]\sidenote{\lorem[7.]}

\bye

sidenote.opm

%% Initialize namespace
\_namespace{sidenote}

%% Initialize some variables _newcount\sidenote_number _newbox.left _newbox.right

%% We just want a unique attribute number, but to get at the number %% itself, we need this hack. _newattribute.attr__ _chardef.attr=_attributealloc

%% Load the Lua module _directlua{require "sidenote"}

%% Add some redefinable styling commands

%% Make the sidenote mark in the text. %% out --> Something to be typeset in the current paragraph %% (will be placed in an \hbox) %% (redefine this if you want a different style) _newpublic_def\sidenote_makemark{% _unskip$^{_the\sidenote_number}$% }

%% Make the sidenote variant for the left margin. %% #1 --> The text of the sidenote %% out --> The contents of the sidenote (will be placed in a \vbox) %% (redefine this if you want a different style) _newpublic_def\sidenote_makeleft#1{% _hsize=2in_relax%% Width of the sidenote _leftskip=0pt plus 1fill_relax%% Ragged left _rightskip=1em_relax%% 1em separation from the paragraphs \sidenote_makemark%% Print the sidenote number #1%% Sidenote text _vskip 1ex_relax%% Make sure succesive sidenotes are 1ex apart }

%% Make the sidenote variant for the left margin. %% #1 --> The text of the sidenote %% out --> The contents of the sidenote (will be placed in a \vbox) %% (redefine this if you want a different style) _newpublic_def\sidenote_makeright#1{% _hsize=2in_relax%% Width of the sidenote _rightskip=0pt plus 1fill_relax%% Ragged right _leftskip=1em_relax%% 1em separation from the paragraphs \sidenote_makemark%% Print the sidenote number #1%% Sidenote text _vskip 1ex_relax%% Make sure succesive sidenotes are 1ex apart }

%% Define the sidenote command %% #1 --> The text of the sidenote %% out --> Typesets the sidenote number with \sidenote_makemark, %% and records the sidenote text to be included at shipout. _newpublic_def\sidenote#1{% _incr\sidenote_number%% Increment the sidenote number _setbox.left=_vbox{%% Save the left sidenote contents \sidenote_makeleft{#1}% }% _setbox.right=_vbox{%% Save the right sidenote contents \sidenote_makeright{#1}% }% % %% Make the sidenote mark and mark it with an attribute so we can %% find it from Lua _hbox attr .attr=\sidenote_number {\sidenote_makemark}% % .savenote%% Run the Lua function }

%% Set the page side _newcount.side

%% Sets the side to typeset the sidenotes on %% #1 --> \left'' or\right'' %% out --> (nothing) _newpublic_def\sidenote_side#1{% \ifx#1_left% _global.side=0% \else% _global.side=1% \fi% }

%% Set the default sidenote side \sidenote_side\left

%% Switch sides automatically for each page % \addto_begoutput{% % \ifodd\pageno% % \sidenote_side\right% % \else% % \sidenote_side\left% % \fi% % }

_endnamespace

sidenote.lua

-- Initialize some constants
local glue_id = node.id("glue")
local hlist_id = node.id("hlist")
local vlist_id = node.id("vlist")
local attribute = token.create("_sidenote_attr").mode
local LEFT = 0

-- Make an \hss now so that we can copy and reuse it later local hss = node.new("glue") hss.stretch = 1 hss.stretch_order = 1 hss.shrink = 1 hss.shrink_order = 1

-- Store the sidenote contents local lefts = {} local rights = {}

-- Save the sidenote contents in Lua so we can access them later define_lua_command( "_sidenote_savenote", function() local number = tex.count.sidenote_number lefts[number] = node.copy_list(tex.box._sidenote_left) rights[number] = node.copy_list(tex.box._sidenote_right) end )

-- Typeset the sidenote contents callback.add_to_callback("pre_shipout_filter", function(head) -- Should we place the sidenotes on the left or on the right? local side = tex.count._sidenote_side

-- The minimum height to place the sidenote at
local min_height = 0
local max_height = (tex.voffset or 0) + tex.vsize

-- Recursively traverse the page
local function recurse(n, height, parent)
    for m in node.traverse(n) do
        -- Set the height of the current node
        local self_height = 0
        if m.id == glue_id and parent.id == vlist_id then
            -- Glue is only vertical if it's in a \vbox
            self_height = node.effective_glue(m, parent)
        elseif (m.height or m.depth) and parent.id == vlist_id then
            self_height = (m.height or 0) + (m.depth or 0)
        end

        -- A node's "shift" attribute is vertical only if the parent is
        -- an \hbox. This corresponds to the primitive \raise.
        local vshift = 0
        if m.shift and
           (parent.id == hlist_id or
            not node.is_node(parent))
        then
            vshift = m.shift
        end

        -- Set the current height
        height = height + self_height

        -- The number of this node's corresponding sidenote
        local attr = node.get_attribute(m, attribute)

        -- We've found a sidenote node
        if attr then
            -- Get the sidenote contents
            local note
            if side == LEFT then
                note = lefts[attr]
            else
                note = rights[attr]
            end

            local note_height = note.height + note.depth

            if min_height + note_height > max_height then
                texio.write_nl(
                    "Warning: sidenote " .. attr .. " is too tall!!!\n"
                )
            end

            -- Zero out the note's height
            note.height = 0
            note.depth = 0

            -- Add any necessary vertical space to prevent collisions
            local offset = math.max(min_height - height, 0)
            local vskip = node.new("glue")
            vskip.width = offset
            note.list = node.insert_before(note.list, note.list, vskip)

            local hss = node.copy(hss)

            -- Create a zero-width \hbox to hold the sidenote
            local box
            if side == LEFT then
                hss.next = note
                box = node.hpack(hss, 0, "exactly")

                lefts[attr] = nil
                node.free(rights[attr])
                rights[attr] = nil
            else
                local offset = node.new("glue")
                offset.width = parent.width

                offset.next = note
                note.next = hss
                box = node.hpack(offset, 0, "exactly")

                rights[attr] = nil
                node.free(lefts[attr])
                lefts[attr] = nil
            end

            -- Inject the \hbox into the page
            parent.prev.next = box
            box.next = parent
            node.slide(parent.prev)

            -- Update the minimum height
            min_height = height + note_height + offset

        -- Not a sidenote, so we need to recurse
        elseif m.list then
            recurse(
                m.list,
                height - self_height + vshift,
                m
            )
        end
    end
end

-- Start at the root of the page
recurse(head.list, tex.voffset or 0, {})

return true

end, "sidenote")

sample output page 1

sample output page 2

Max Chernoff
  • 4,667
  • 3
    This is great. Lua is powerful tool. – wipet Jun 27 '23 at 04:49
  • This is really brilliant. Thank you so much (again), Max! – user574859 Jun 27 '23 at 14:06
  • I noticed that the note number is the same style as the footnote: it uses the math mode font. I tried to find out in the files where I could change that (using the fnmark defined in my question, but it eludes me. Any hints? – user574859 Jun 27 '23 at 15:02
  • For the same reason, I can't restart the side note number every chapter... – user574859 Jun 27 '23 at 15:16
  • @user574859 Ok, I've updated the code. You just need to redefine \sidenote_makemark; I've added an example. – Max Chernoff Jun 30 '23 at 08:19
  • @MaxChernoff, thanks again! The only odd thing is that I had to rename sidenote.opm to sidenote.opm.tex for it to work. Maybe an issue with the Texworks editor, who knows. – user574859 Jun 30 '23 at 10:11
  • Regards. This works in Overleaf? – juanuni Aug 13 '23 at 19:42
  • @juanuni It should, provided that you're able to run OpTeX in Overleaf (which is possible but tricky) – Max Chernoff Aug 13 '23 at 21:39
  • In Overleaf appears: Undefined control sequence l.25\_newpublic\_def\sidenote_makemark{% for sidenote.opm. Similar for for l.48 \_newpublic\_def\sidenote_makeright#1{% and l.86 \_newpublic\_def\sidenote_side#1{% – juanuni Aug 13 '23 at 21:44
  • @juanuni That suggests that you're using the LaTeX compiler. This is the process to use OpTeX with Overleaf, although you'd probably be better off sticking with LaTeX (and using a different sidenote solution) – Max Chernoff Aug 13 '23 at 21:50
  • Well I was using LuaLaTeX compiler in Overleaf. Looks like a problem due to the OpTeX version in Overleaf. – juanuni Aug 13 '23 at 21:57
  • 2
    Well, this works perfectly now in Overleaf due this available TeXLive 2023. – juanuni Sep 10 '23 at 08:04
  • I don't know why, but \addto\_begoutput{% \ifodd\pageno% \sidenote_side\right% \else% \sidenote_side\left% \fi% } doesn't work anymore on LuaTeX (Texlive 2023) . – user574859 Sep 17 '23 at 17:40
  • @user574859 I'm tested it again with an up-to-date TL23 and everything still works for me. – Max Chernoff Sep 17 '23 at 22:49
  • I have noticed that this solution also affects the initial pages such as the title page or table of contents page. How to ensure that it does not affect pages at the beginning of a document or even the pages reserved for Parts? – juanuni Jan 01 '24 at 17:21
  • @juanuni I'm not sure what you mean exactly. I don't think the posted solution does anything unless you explicitly place a sidenote, but if you place a sidenote on the title page then you'll unsurprisingly get a sidenote. If you're talking about the double-sided margins, that's from the \margins/2 … command. – Max Chernoff Jan 06 '24 at 00:20
  • Well, I was using the command \tit and \author but I don't know how to locate them so that your solution does not affect them. In fact, I'm wanting to include a \part and also not have a sidenote on the page where it goes, but I suspect it's more complicated. – juanuni Jan 07 '24 at 14:09
  • @juanuni Ah, ok; that makes sense now. Probably best to post that as a new question and link to it from here. – Max Chernoff Jan 08 '24 at 05:08
  • I have noticed that in the sidenote of the even pages the text on each line appears from right to left. How could the text appear in reverse on even-numbered pages? Is it possible to justify the text within the odd or even page sidenote? – juanuni Feb 07 '24 at 07:13
  • @juanuni The justification is from the \leftskip and \rightskip bits. Anything with a plus makes that edge ragged, anything without a plus makes that edge justified. So \leftskip=0pt \rightskip=0pt is fully-justified, \leftskip=0pt plus 1fil \rightskip=0pt plus 1fil is centered, \leftskip=0pt \rightskip=0pt plus 1fil is flush left/ragged right, etc. If you're going to try justified, you'll probably need to raise the \tolerance by quite a bit, and even then I'd still expect lots of over/underfull boxes. – Max Chernoff Feb 07 '24 at 07:35