Como evitar quebras de linha que resultam em palavras curtas nas bordas das linhas?

Como evitar quebras de linha que resultam em palavras curtas nas bordas das linhas?

Eu queria saber se existe uma maneira automática de evitar palavras curtas nas bordas das linhas - evitando especificamente palavras curtas após a pontuação na borda direita e palavras curtas antes da pontuação na borda esquerda?

Por exemplo, se uma frase começa com Iou If, ou há um anddireito após uma vírgula, podemos fazer com que o látex a empurre para a próxima linha automaticamente, mantendo as regras básicas de espaçamento?Essa questãoé semelhante, mas parece que a solução ainda requer uma correção manual (a menos que eu esteja interpretando errado).

(Estou trabalhando em pdflatex)

Responder1

Existem dois objetivos aqui:

  • não interrompa após uma palavra curta que segue imediatamente a pontuação,
  • não interrompa antes de uma palavra curta que precede imediatamente a pontuação,

sujeito a restrições regulares de boa quebra de linha.

Uma solução simples é declarar a pontuação como um lugar particularmente bom para quebrar (uma penalidade negativa, de magnitude suficientemente grande). Isso permitirá que o TeX negocie a tentativa de quebra de pontuação com suas outras considerações de quebra de linha (maldade, deméritos, outras penalidades), mas não garantirá que não haja absolutamente nenhuma quebra desse tipo.

Aqui está um antes e depois, para ilustrar:

antes-depois-anotado

Como você pode ver,

  • No primeiro parágrafo, o , itfinal da terceira linha passou para a próxima linha após a alteração.
  • No segundo parágrafo, o que el.está no início da quarta linha e o que está at,no início da sexta linha passou para a linha anterior após a alteração.
  • O terceiro parágrafo foi incluído para mostrar que esse truque não é uma garantia: o que it.está no início da quarta linha permanece lá, porque simplesmente não há como encaixá-lo na linha anterior.

Isto foi conseguido com:

\catcode`.=\active \def.{\char`.\penalty -200\relax}
\catcode`,=\active \def,{\char`,\penalty -200\relax}

no seguinte documento:

\documentclass{article}
\begin{document}
\frenchspacing % Makes it easier
\hsize=20em
\parskip=10pt

% First, three paragraphs with the default settings
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut blandit placerat justo, sed dictum sem. Donec erat elit, tincidunt non, it vel, tincidunt vehicula velit. Etiam pharetra ante at porta elementum. In nulla purus, faucibus non accumsan non, consequat eget.

Natis nulla blandit luctus tellus, sit amet posuere lacus maxius quis. In sit amet mattis est, a vehiula velit. Nam interum solicitudin el. In faucibus vulputate purus nec consectelur crass metus ipsum, blandit iln ullamcorpert at, portitor vita dolor. Duis sed mauris i inset inculis malesuada. Quisque laoret eu dui eget sage melittis corpum verborum.

Volutpat libero ac auctor. Donec semper, as id ultrices rhoncus, lectus nulla consequat nisi, ac sagitis risus lectus vel felis. Ut gravida it. Nam malesuada ante turpis eget. Ipsum factum verbum verdit.

\pagebreak

% Now the same text, with the meanings of . and , changed.
\catcode`.=\active \def.{\char`.\penalty -200\relax}
\catcode`,=\active \def,{\char`,\penalty -200\relax}

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut blandit placerat justo, sed dictum sem. Donec erat elit, tincidunt non, it vel, tincidunt vehicula velit. Etiam pharetra ante at porta elementum. In nulla purus, faucibus non accumsan non, consequat eget.

Natis nulla blandit luctus tellus, sit amet posuere lacus maxius quis. In sit amet mattis est, a vehiula velit. Nam interum solicitudin el. In faucibus vulputate purus nec consectelur crass metus ipsum, blandit iln ullamcorpert at, portitor vita dolor. Duis sed mauris i inset inculis malesuada. Quisque laoret eu dui eget sage melittis corpum verborum.

Volutpat libero ac auctor. Donec semper, as id ultrices rhoncus, lectus nulla consequat nisi, ac sagitis risus lectus vel felis. Ut gravida it. Nam malesuada ante turpis eget. Ipsum factum verbum verdit.

% Change it back
\catcode`.=12 \catcode`,=12
\pagebreak

% Same text again, to show that nothing's permanently changed.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut blandit placerat justo, sed dictum sem. Donec erat elit, tincidunt non, it vel, tincidunt vehicula velit. Etiam pharetra ante at porta elementum. In nulla purus, faucibus non accumsan non, consequat eget.

Natis nulla blandit luctus tellus, sit amet posuere lacus maxius quis. In sit amet mattis est, a vehiula velit. Nam interum solicitudin el. In faucibus vulputate purus nec consectelur crass metus ipsum, blandit iln ullamcorpert at, portitor vita dolor. Duis sed mauris i inset inculis malesuada. Quisque laoret eu dui eget sage melittis corpum verborum.

Volutpat libero ac auctor. Donec semper, as id ultrices rhoncus, lectus nulla consequat nisi, ac sagitis risus lectus vel felis. Ut gravida it. Nam malesuada ante turpis eget. Ipsum factum verbum verdit.

\end{document}

Notas:

  • Eu não ficaria surpreso se mudar os significados de .e ,assim quebrasse alguma coisa. (Na verdade, fiquei surpreso que nada tenha errado neste exemplo, então percebi que as alterações no catcode não se aplicam a tokens que já foram lidos.)
  • Você pode ajustar as penalidades: usei -200 apenas como exemplo, mas qualquer coisa entre -1 e -9999 teráalgunsefeito. (Neste exemplo, o limite para que todas essas alterações entrem em vigor parece ser -175, embora uma alteração aconteça mesmo em -100.) Uma penalidade ≤ -10000 força uma quebra de linha, o que não é o que você deseja.
  • Você pode fazer o mesmo para mais caracteres de pontuação ( ?!:;) ou aplicar penalidades diferentes para caracteres de pontuação diferentes.
  • As coisas são um pouco mais difíceis com \nonfrenchspacing(o padrão), onde os espaços são maiores após a pontuação. Pode ser factível, mas criar esses exemplos deu muito trabalho, então não continuei. Deixado como exercício :-)
  • Com LuaTeX você pode até alterar o algoritmo de quebra de linha, o que seria uma maneira legal degarantiasem palavras curtas nas bordas das linhas (se é isso que você precisa).

Editar: Não resisti em implementar a solução “garantida” no LuaTeX. Esta versão deve funcionar com \frenchspacinge \nonfrenchspacing. O que ele faz é detectar certas sequências e inserir penalidades infinitas (10.000) para evitar uma quebra:

(punct, space, short_word, space) -> (punct, space, short_word, penalty, space)

e

(space, short_word, punct) -> (penalty, space, short_word, punct)

Para o exemplo acima, isso produz:

com LuaTeX

Observe a caixa cheia demais no último parágrafo porque as restrições são bastante rígidas, mas foi isso que pedimos. (De qualquer forma, você provavelmente não terá caixas muito cheias com parágrafos mais largos e longos, e poderá corrigi-los da maneira usual, reescrevendo ou adicionando \emergencystretche assim por diante.)

O código que produziu o texto acima (e até mesmo a ideia) possivelmente contém bugs que podem até causar o travamento da sua compilação LuaTeX, mas aqui está:

\documentclass{article}
\directlua{dofile("strict.lua")}
\begin{document}
\frenchspacing % Keeping same example as before
\hsize=20em
\parskip=10pt

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut blandit placerat justo, sed dictum sem. Donec erat elit, tincidunt non, it vel, tincidunt vehicula velit. Etiam pharetra ante at porta elementum. In nulla purus, faucibus non accumsan non, consequat eget.

Natis nulla blandit luctus tellus, sit amet posuere lacus maxius quis. In sit amet mattis est, a vehiula velit. Nam interum solicitudin el. In faucibus vulputate purus nec consectelur crass metus ipsum, blandit iln ullamcorpert at, portitor vita dolor. Duis sed mauris i inset inculis malesuada. Quisque laoret eu dui eget sage melittis corpum verborum.

Volutpat libero ac auctor. Donec semper, as id ultrices rhoncus, lectus nulla consequat nisi, ac sagitis risus lectus vel felis. Ut gravida it. Nam malesuada ante turpis eget. Ipsum factum verbum verdit.
\end{document}

Onde strict.luaé:

function is_punct(n)
   if node.type(n.id) ~= 'glyph' then return false end
   if n.char > 127 then return false end
   c = string.char(n.char)
   if c == '.' or c =='?' or c == '!' or c == ':' or c == ';' or c == ',' then
      return true
   end
   return false
end

function no_punct_short_word_eol(head)
   -- Prevents having a line that ends like "<punctuation><space><short_word>"
   -- How we do this:
   --   (1) detect such short words (punct, space, short_word, space)
   --   (2) insert a penalty of 10000 between the short_word and the following space.
   -- More concretely:
   --   * A punctuation is one of .?!:;, which are the ones affected by \frenchspacing
   --   * A space is any glue node.
   --   * A short_word is a sequence of only glyph and kern nodes.
   -- So we maintain a state machine: default -> seen_punct -> seen_space -> seen_word
   -- where in the last state we maintain length. If we're in seen_word state and we see
   -- a glue, and length is less than threshold, insert a penalty before the glue.
   state = 'default'
   root = head
   while head do
      if state == 'default' then
         if is_punct(head) then
            state = 'seen_punct'
         end
      elseif state == 'seen_punct' then
         if node.type(head.id) == 'glue' then
            state = 'seen_space'
         else
            state = 'default'
         end
      elseif state == 'seen_space' then
         if node.type(head.id) == 'glyph' then
            state = 'seen_word'
            length = 1
         elseif is_punct(head) then
            state = 'seen_punct'
         else
            state = 'default'
         end
      elseif state == 'seen_word' then
         if node.type(head.id) == 'glue' and length <= 2 then
            -- Moment of truth
            penalty = node.new('penalty')
            penalty.penalty = 10000
            root, new = node.insert_before(root, head, penalty)
            -- TODO: Is 'head' invalidated now? Docs don't say anything...
            state = 'default'
         elseif node.type(head.id) == 'glyph' or node.type(head.id) == 'kern' then
            if node.type(head.id) == 'glyph' then length = length + 1 end
         else
            state = 'default'
         end
      else
         assert(false, string.format('Impossible state %s', state))
      end
      head = head.next
   end
   return root
end
luatexbase.add_to_callback('pre_linebreak_filter', no_punct_short_word_eol, 'Prevent short words after punctuation at end of sentence')

function no_bol_short_word_punct(head)
   -- Prevents having a line that starts like "<short_word><punctuation>"
   -- How we do this:
   --   (1) detect such short words (space, short_word, punct)
   --   (2) insert a penalty of 10000 between the space and the following short_word.
   -- More concretely:
   --   * A punctuation is one of .?!:;, which are the ones affected by \frenchspacing
   --   * A space is any glue node.
   --   * A short_word is a sequence of only glyph and kern nodes.
   -- So we maintain a state machine: default -> seen_space -> seen_word
   -- where in the last state we maintain length. If we're in seen_word state and we see
   -- a punct, and length is less than threshold, insert a penalty before the glue.
   -- Note that for this to work, we need to maintain a pointer to where we saw the glue.
   state = 'default'
   root = head
   before_space = nil
   while head do
      if state == 'default' then
         if node.type(head.id) == 'glue' then
            state = 'seen_space'
            before_space = head.prev
         end
      elseif state == 'seen_space' then
         if node.type(head.id) == 'glyph' then
            state = 'seen_word'
            length = 1
         else
            state = 'default'
         end
      elseif state == 'seen_word' then
         if is_punct(head) and length <= 2 then
            -- Moment of truth
            penalty = node.new('penalty')
            penalty.penalty = 10000
            root, new = node.insert_after(root, before_space, penalty)
            -- TODO: Is 'head' invalidated now? Docs don't say anything...
            state = 'default'
         elseif node.type(head.id) == 'glyph' or node.type(head.id) == 'kern' then
            if node.type(head.id) == 'glyph' then length = length + 1 end
         elseif node.type(head.id) == 'glue' then
            state = 'seen_space'
            before_space = head.prev
         else
            state = 'default'
         end
      else
         assert(false, string.format('Impossible state %s', state))
      end
      head = head.next
   end
   return root
end
luatexbase.add_to_callback('pre_linebreak_filter', no_bol_short_word_punct, 'Prevent short words at beginning of sentence before punctuation')

informação relacionada