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")


reffile 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:44scrlayer-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