Microtype quotation marks in itemize not aligned properly at begin of an item

itemizemicrotypeprotrusionquotation

A fellow LaTeX user ran into a problem using quotations inside an itemize/enumerate environment. When using automatic quotation marks from csquotes at the beginning of an item, the quotation mark is not protruded properly into the "margin". Is there a way to fix this in a sustainable way?

Ideally, I am trying to find a solution which would consist of some code that I can include in the preamble of my document, following which every instance of \item "quotes" will automatically have the right protrusion.
Otherwise, I will have to manually change several hundred quotes in my document, which will be very painful.

LaTeX Version

pdfTeX 3.141592653-2.6-1.40.23 (TeX Live 2022/dev)

LuaLaTeX Version (This is what is used in this MWP, albeit it doesn't seem to make a difference)

This is LuaHBTeX, Version 1.13.2 (TeX Live 2021)

Minimal Working example:

%!TEX TS-program = lualatex

\documentclass[12pt]{article}
\usepackage{fontspec}
\usepackage{libertinus-otf}
\usepackage{lipsum}
\usepackage{csquotes} \MakeOuterQuote{"}
\usepackage{microtype}
\SetProtrusion
{ encoding = *}
{
% char   right left
  {.} = {    , 1000},
  {,} = {    , 1000},
  {«} = {1000,     },
  {»} = {    , 1000},
  {(} = {1000,     },
  {)} = {    , 1000},
  {-} = {    , 500 },
  \textquotedblleft
      = {1000,     },
  \textquotedblright
      = {    , 1000},
  \quotedblbase
      = {1000,     }
}
\begin{document}
\noindent
"\lipsum[1-1]"

\begin{enumerate}
  \item First regular item, without quotations.
  \item "Second item with quotation marks."
  \item First multi-line item, contains content that is deliberately very, very long: "with multiple clauses that are designed to trigger a line break," so that it will wrap around and create multiple rows.
  \item "Second multi-line item, but keep in mind it is different from the first since it is quoted. However it also contains content that is deliberately very, very long: with multiple clauses just like the first."
  \item "Multiple quoted lines with a single item"
  \item \leftprotrusion 

  "This is the second quoted line"

  This is a third unquoted line.
\end{enumerate}
\end{document}

Attempts to fix:
Adding \items in the following way instead seems to fix the problem:

\item \leftprotrusion ``quotes''

However, this would mean a lot of manual adjustment. (As a side-note, this workaround also doesn't work in conjunction with csquotes, meaning \item \leftprotrusion "something in quotes" doesn't give the right result either.)

Visualisation of the wrongly aligned quotation marks at the start of an item in a list

Best Answer

I don't have time for an explanation right now, so for now I'll just add the code. (If you read this after 2021-01-10 and this note has not been replaced by a proper explanation by now, feel free to remind me.)

Requires LuaLaTeX.

%!TEX TS-program = lualatex

\documentclass[12pt]{article}
\usepackage{fontspec}
\usepackage{libertinus-otf}
\usepackage{lipsum}
\usepackage{csquotes} \MakeOuterQuote{"}
\usepackage{microtype}
\SetProtrusion
{ encoding = *}
{
% char   right left
  {.} = {    , 1000},
  {,} = {    , 1000},
  {«} = {1000,     },
  {»} = {    , 1000},
  {(} = {1000,     },
  {)} = {    , 1000},
  {-} = {    , 500 },
  \textquotedblleft
      = {1000,     },
  \textquotedblright
      = {    , 1000},
  \quotedblbase
      = {1000,     }
}

\directlua{
  local func = luatexbase.new_luafunction'betterprotrusionboundary'
  local my_whatsit = luatexbase.new_whatsit'betterprotrusionboundary'
  local whatsit_id = node.id'whatsit'
  local glyph_id = node.id'glyph'
  local user_defined = node.subtype'user_defined'
  token.set_lua('betterprotrusionboundary', func, 'protected')
  local modes = tex.getmodevalues()
  lua.get_functions_table()[func] = function()
    local mode = tex.nest.top.mode
    if mode < 0 then mode = -mode end
    if modes[mode] == 'vertical' then
    token.put_next(token.new(func, token.command_id'lua_call'))
      return tex.forcehmode()
    end
    local n = node.new(whatsit_id, user_defined)
    n.user_id = my_whatsit
    n.type = 100
    n.value = token.scan_int()
    node.write(n)
  end

  luatexbase.add_to_callback('pre_linebreak_filter', function(head)
    for n, s in node.traverse_id(whatsit_id, head) do if s == user_defined and n.user_id == my_whatsit then
      assert(n.value == 1, 'boundarytypes beside 1 not yet supported')
      if n.value & 1 == 1 then
        for nn, id in node.traverse(n.next) do
          local char, fid = node.is_glyph(nn)
          if char then
            token.put_next(token.create'lpcode', token.new(fid, token.command_id'set_font'), token.new(char, token.command_id'char_given'))
            local width = (font.getparameters(fid).quad or 0) * token.scan_int() // 1000
            if not (width == 0) then
              local kern = node.new('kern', 1)
              % local kern = node.new('margin_kern', 0)
              kern.kern = -width
              % kern.glyph = char
              head = node.insert_after(head, n, kern)
            end
            break
          elseif not node.protrusion_skippable(nn) then
            break
          elseif fid == whatsit_id and nn.subtype == user_defined and nn.user_id == my_whatsit and nn.value & 1 == 1 then
            break
          end
        end
      end
      if n.value & 2 == 2 then
        local nn = n.prev
        while nn do
          local char, fid = node.is_glyph(nn)
          if char then
            token.put_next(token.create'rpcode', token.new(fid, token.command_id'set_font'), token.new(char, token.command_id'char_given'))
            local width = (font.getparameters(fid).quad or 0) * token.scan_int() // 1000
            if not (width == 0) then
              local kern = node.new('kern', 1)
              % local kern = node.new('margin_kern', 1)
              kern.kern = -width
              % kern.glyph = char
              head = node.insert_before(head, n, kern)
            end
            break
          elseif not node.protrusion_skippable(nn) then
            break
          end
          nn = nn.prev
        end
      end
      head = node.remove(head, n)
    end end
    return head
  end, 'betterprotrusionboundary')
}

\begin{document}
\showoutput
\renewcommand\leftprotrusion{\betterprotrusionboundary1\relax}
\noindent
"\lipsum[1-1]"

\begin{enumerate}
  \item First regular item, without quotations.
  \item "Second item with quotation marks."
  \item First multi-line item, contains content that is deliberately very, very long: "with multiple clauses that are designed to trigger a line break," so that it will wrap around and create multiple rows.
  \item "Second multi-line item, but keep in mind it is different from the first since it is quoted. However it also contains content that is deliberately very, very long: with multiple clauses just like the first."
  \item "Multiple quoted lines with a single item"
  \item \leftprotrusion ``quotes''

  "This is the second quoted line"

  This is a third unquoted line.
\end{enumerate}
\end{document}

enter image description here

Related Question