[FIX] разреженные строки: переносы в составных словах и в инлайн-коде
This commit is contained in:
@@ -33,11 +33,43 @@ def escape_latex(text: str) -> str:
|
|||||||
return text.translate(_LATEX_SPECIAL)
|
return text.translate(_LATEX_SPECIAL)
|
||||||
|
|
||||||
|
|
||||||
# Инициалы не должны отрываться друг от друга и от фамилии переносом строки:
|
# Длина слова, начиная с которой ему нужны точки переноса, и символы, после
|
||||||
# «/ И.» на одной строке и «В. Гилев» на следующей — ошибка оформления списка
|
# которых переносить допустимо. Неразрывный идентификатор вроде
|
||||||
# источников. Связывается одиночная заглавная буква с точкой и следующее слово,
|
# QT_QPA_PLATFORM=offscreen не помещается в остаток строки целиком, и абзац
|
||||||
# начинающееся с заглавной.
|
# приходится разгонять пробелами.
|
||||||
_INITIALS_RE = re.compile(r"(?<![\w.])([A-ZА-ЯЁ]\.)\s+(?=[A-ZА-ЯЁ])")
|
LONG_TOKEN = 12
|
||||||
|
BREAK_AFTER = ":/_.-="
|
||||||
|
|
||||||
|
|
||||||
|
def allow_breaks(word: str) -> str:
|
||||||
|
"""Вставить \\allowbreak после разделителей внутри длинного слова."""
|
||||||
|
if len(word) < LONG_TOKEN:
|
||||||
|
return word
|
||||||
|
out = []
|
||||||
|
for ch in word:
|
||||||
|
out.append(ch)
|
||||||
|
if ch in BREAK_AFTER:
|
||||||
|
out.append("\\allowbreak ")
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
# Инициалы не должны разрываться переносом строки: «/ И.» на одной строке и
|
||||||
|
# «В.» на следующей — ошибка оформления списка источников. Связываются только
|
||||||
|
# инициалы между собой: следующее слово с заглавной буквы может оказаться не
|
||||||
|
# фамилией, а началом названия работы, и тогда неразрывная цепочка получается
|
||||||
|
# такой длинной, что распирает строку.
|
||||||
|
_INITIALS_RE = re.compile(r"(?<![\w.])([A-ZА-ЯЁ]\.)\s+(?=[A-ZА-ЯЁ]\.)")
|
||||||
|
|
||||||
|
|
||||||
|
# TeX не переносит слово, содержащее дефис, — «программно-аппаратного» целиком
|
||||||
|
# не влезает в строку, и абзац приходится разгонять пробелами вдвое шире нормы.
|
||||||
|
# Шорткат babel «"=» ставит тот же дефис, но разрешает переносы в обеих частях.
|
||||||
|
_COMPOUND_RE = re.compile(r"(?<=[а-яёА-ЯЁ])-(?=[а-яёА-ЯЁ])")
|
||||||
|
|
||||||
|
|
||||||
|
def split_compounds(text: str) -> str:
|
||||||
|
"""Разрешить переносы в составных словах через дефис."""
|
||||||
|
return _COMPOUND_RE.sub('"=', text)
|
||||||
|
|
||||||
|
|
||||||
def bind_initials(text: str) -> str:
|
def bind_initials(text: str) -> str:
|
||||||
@@ -51,7 +83,7 @@ def bind_initials(text: str) -> str:
|
|||||||
|
|
||||||
@inline("text")
|
@inline("text")
|
||||||
def _text(tok: Token) -> str:
|
def _text(tok: Token) -> str:
|
||||||
return bind_initials(escape_latex(tok.content))
|
return split_compounds(bind_initials(escape_latex(tok.content)))
|
||||||
|
|
||||||
|
|
||||||
@inline("strong_open")
|
@inline("strong_open")
|
||||||
@@ -76,7 +108,8 @@ def _em_close(tok: Token) -> str:
|
|||||||
|
|
||||||
@inline("code_inline")
|
@inline("code_inline")
|
||||||
def _code_inline(tok: Token) -> str:
|
def _code_inline(tok: Token) -> str:
|
||||||
return f"\\texttt{{\\small {escape_latex(tok.content)}}}"
|
content = " ".join(allow_breaks(w) for w in escape_latex(tok.content).split(" "))
|
||||||
|
return f"\\texttt{{\\small {content}}}"
|
||||||
|
|
||||||
|
|
||||||
@inline("link_open")
|
@inline("link_open")
|
||||||
|
|||||||
@@ -28,15 +28,11 @@ import re
|
|||||||
|
|
||||||
from markdown_it.token import Token
|
from markdown_it.token import Token
|
||||||
|
|
||||||
from md2gost.handlers.inline import escape_latex, render_inline
|
from md2gost.handlers.inline import allow_breaks, escape_latex, render_inline
|
||||||
from md2gost.registry import block, pattern
|
from md2gost.registry import block, pattern
|
||||||
|
|
||||||
_CAPTION_RE = re.compile(r"^\s*Таблица\s+\d+\s*[—–-]+\s*(.+?)\s*$")
|
_CAPTION_RE = re.compile(r"^\s*Таблица\s+\d+\s*[—–-]+\s*(.+?)\s*$")
|
||||||
|
|
||||||
# Длина слова, начиная с которой ему нужны точки разрыва. Идентификаторы вроде
|
|
||||||
# HackRFTestingServer:идентификатор не содержат пробелов и дефисов, поэтому в
|
|
||||||
# узкой колонке LaTeX не может их перенести: строка уезжает за границу колонки
|
|
||||||
# и печатается поверх соседней.
|
|
||||||
_LONG_WORD = 18
|
_LONG_WORD = 18
|
||||||
|
|
||||||
# Колонка, где самое длинное значение короче этого, в растягивании не нуждается:
|
# Колонка, где самое длинное значение короче этого, в растягивании не нуждается:
|
||||||
@@ -58,18 +54,10 @@ def _breakable(cell: str) -> str:
|
|||||||
Ячейка уже отрендерена в LaTeX, поэтому слова с обратной косой чертой или
|
Ячейка уже отрендерена в LaTeX, поэтому слова с обратной косой чертой или
|
||||||
фигурными скобками пропускаются: вставка внутрь имени команды сломала бы её.
|
фигурными скобками пропускаются: вставка внутрь имени команды сломала бы её.
|
||||||
"""
|
"""
|
||||||
words = []
|
return " ".join(
|
||||||
for word in cell.split(" "):
|
word if any(ch in word for ch in "\\{}") else allow_breaks(word)
|
||||||
if len(word) < _LONG_WORD or any(ch in word for ch in "\\{}"):
|
for word in cell.split(" ")
|
||||||
words.append(word)
|
)
|
||||||
continue
|
|
||||||
out = []
|
|
||||||
for ch in word:
|
|
||||||
out.append(ch)
|
|
||||||
if ch in _BREAK_AFTER:
|
|
||||||
out.append("\\allowbreak ")
|
|
||||||
words.append("".join(out))
|
|
||||||
return " ".join(words)
|
|
||||||
|
|
||||||
|
|
||||||
def _cell_align(tok: Token) -> str:
|
def _cell_align(tok: Token) -> str:
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ def parse_log(
|
|||||||
|
|
||||||
|
|
||||||
def _describe(diag: Diagnostic) -> str:
|
def _describe(diag: Diagnostic) -> str:
|
||||||
where = f"строка {diag.line}" if diag.line is not None else "место не указано"
|
where = (
|
||||||
|
f"абзац в строке {diag.line}" if diag.line is not None else "место не указано"
|
||||||
|
)
|
||||||
if diag.kind == "overfull":
|
if diag.kind == "overfull":
|
||||||
return f" {where}: на {diag.amount:.1f} pt шире поля"
|
return f" {where}: на {diag.amount:.1f} pt шире поля"
|
||||||
if diag.kind == "underfull":
|
if diag.kind == "underfull":
|
||||||
|
|||||||
@@ -26,8 +26,11 @@ PREAMBLE = r"""\documentclass[a4paper, 14pt]{extarticle}
|
|||||||
\setcounter{bottomnumber}{2}
|
\setcounter{bottomnumber}{2}
|
||||||
\setcounter{totalnumber}{5}
|
\setcounter{totalnumber}{5}
|
||||||
\usepackage{wrapfig}
|
\usepackage{wrapfig}
|
||||||
\sloppy
|
% \sloppy не используется намеренно: он снимает штраф за разреженные строки
|
||||||
\setlength{\emergencystretch}{2em}
|
% целиком, и абзац с длинным неразрывным словом разгоняется пробелами вдвое
|
||||||
|
% шире нормальных. \emergencystretch включается только там, где иначе строка
|
||||||
|
% вышла бы за поле.
|
||||||
|
\setlength{\emergencystretch}{3em}
|
||||||
\clubpenalty=10000
|
\clubpenalty=10000
|
||||||
\widowpenalty=10000
|
\widowpenalty=10000
|
||||||
\usepackage{enumitem}
|
\usepackage{enumitem}
|
||||||
|
|||||||
+15
-2
@@ -501,11 +501,24 @@ def test_bare_listing_does_not_float() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_initials_are_not_split_across_lines() -> None:
|
def test_initials_are_not_split_across_lines() -> None:
|
||||||
"""«/ И.» на одной строке и «В. Гилев» на следующей — ошибка оформления."""
|
"""«/ И.» на одной строке и «В.» на следующей — ошибка оформления."""
|
||||||
tex = body(render("Гилев, И. В. Использование технологии SDR.\n"))
|
tex = body(render("Гилев, И. В. Использование технологии SDR.\n"))
|
||||||
assert "И.~В.~Использование" in tex
|
assert "И.~В. Использование" in tex
|
||||||
|
|
||||||
|
|
||||||
def test_lowercase_abbreviation_is_left_alone() -> None:
|
def test_lowercase_abbreviation_is_left_alone() -> None:
|
||||||
tex = body(render("В 3 т., Воронеж, 2025.\n"))
|
tex = body(render("В 3 т., Воронеж, 2025.\n"))
|
||||||
assert "т.~Воронеж" not in tex
|
assert "т.~Воронеж" not in tex
|
||||||
|
|
||||||
|
|
||||||
|
def test_compound_word_allows_hyphenation() -> None:
|
||||||
|
"""TeX не переносит слово с дефисом, и абзац разгоняется пробелами."""
|
||||||
|
tex = body(render("Разработка программно-аппаратного комплекса.\n"))
|
||||||
|
assert 'программно"=аппаратного' in tex
|
||||||
|
|
||||||
|
|
||||||
|
def test_latin_hyphen_is_left_alone() -> None:
|
||||||
|
"""Шорткат babel рассчитан на русский; латинские дефисы не трогаем."""
|
||||||
|
tex = body(render("Инструмент clang-format и Wayland-сессия.\n"))
|
||||||
|
assert "clang-format" in tex
|
||||||
|
assert 'clang"=format' not in tex
|
||||||
|
|||||||
Reference in New Issue
Block a user