[ADD] пометка правок ==текст== жёлтым выделением

Рецензент просит показывать в отчёте только исправленные места, чтобы не
перечитывать текст целиком. Разметка разбирается на уровне текстовых узлов,
поэтому «==» в листингах на C++ остаётся сравнением.

Выделяется каждое слово отдельно: одной плашкой на всё выделение строка не
разорвётся по его середине, а выделять приходится и целые предложения.
Составное слово делится по дефису — плашка неразрывна, и шорткат переноса
внутри неё не работает. soul не подошёл: под XeLaTeX его \hl теряет кириллицу.
This commit is contained in:
Arseny
2026-09-07 17:06:17 +03:00
parent be64bf475f
commit fa440049e1
3 changed files with 66 additions and 1 deletions
+1
View File
@@ -51,6 +51,7 @@ docker build -t md2gost .
- Изображения (см. ниже)
- Аббревиатуры (раздел "Перечень принятых сокращений")
- Ссылки, жирный, курсив
- Пометка правок `==текст==` — жёлтое выделение для рецензента
### Изображения
+41 -1
View File
@@ -81,9 +81,49 @@ def bind_initials(text: str) -> str:
return text
# ==текст== — пометка правки для рецензента. Разметки такого рода в
# CommonMark нет, поэтому она разбирается здесь, на уровне текстовых узлов:
# внутрь листингов и инлайн-кода регулярное выражение не попадает, и «==»
# в исходниках на C++ остаётся сравнением. Границы выделения примыкают к
# тексту вплотную — как у прочих парных знаков разметки.
_MARK_RE = re.compile(r"==(?=\S)(.+?)(?<=\S)==")
def render_text(text: str) -> str:
return split_compounds(bind_initials(escape_latex(text)))
def highlight_word(word: str) -> str:
"""Слово в жёлтой плашке, составное — по плашке на каждую часть.
Плашка неразрывна, поэтому шорткат переноса «"=» внутри неё не работает
и «аппаратно-программный» целиком вылезает за поле. Дефис получает свою
плашку, а точка переноса ставится после неё.
"""
parts = [part for part in word.split("-") if part]
return "\\hlw{-}\\allowbreak ".join(f"\\hlw{{{part}}}" for part in parts)
def highlight(text: str) -> str:
"""Жёлтая плашка на каждом слове.
Одной плашкой на всё выделение строка не разорвётся по его середине,
а выделять приходится и целые предложения.
"""
escaped = bind_initials(escape_latex(text))
return " ".join(highlight_word(word) for word in escaped.split(" ") if word)
@inline("text")
def _text(tok: Token) -> str:
return split_compounds(bind_initials(escape_latex(tok.content)))
parts: list[str] = []
end = 0
for match in _MARK_RE.finditer(tok.content):
parts.append(render_text(tok.content[end : match.start()]))
parts.append(highlight(match.group(1)))
end = match.end()
parts.append(render_text(tok.content[end:]))
return "".join(parts)
@inline("strong_open")
+24
View File
@@ -37,6 +37,30 @@ def test_unnumbered_heading() -> None:
assert "ВВЕДЕНИЕ" in tex
def test_mark_highlights_every_word() -> None:
tex = render("Обычный ==правленый текст== дальше.\n")
assert "\\hlw{правленый} \\hlw{текст}" in tex
assert "Обычный " in tex
assert " дальше." in tex
def test_mark_splits_compound_word_into_separate_boxes() -> None:
# Плашка неразрывна: составное слово одним боксом вылезает за поле.
tex = render("==аппаратно-программный комплекс==\n")
assert "\\hlw{аппаратно}\\hlw{-}\\allowbreak \\hlw{программный}" in tex
def test_mark_in_heading_reaches_the_contents() -> None:
tex = render("## ==Контейнеризация серверного приложения==\n")
assert "\\subsection{\\hlw{Контейнеризация}" in tex
def test_comparison_in_listing_is_not_a_mark() -> None:
tex = body(render("```cpp\nif (a==b && c==d) return;\n```\n"))
assert "\\hlw" not in tex
assert "a==b" in tex
def test_list_punctuation() -> None:
tex = render("- один\n- два\n- три\n")
assert " \\item один;" in tex