Вам нужно будет проанализировать строку до запрошенной позиции и, если она находится в допустимой паре разделителей среды LaTeX, до закрывающего разделителя, чтобы иметь возможность ответить с помощью True
или False
.Это потому, что вы должны обработать каждый соответствующий метасимвол (обратную косую черту, доллары и скобки), чтобы определить их эффект.
Я понял, что Разделители среды $...$
и \(...\)
латекса могут 'не быть вложенным, поэтому вам не нужно беспокоиться о вложенных выражениях здесь;вам нужно только найти ближайшую полную пару $...$
или \(...\)
.
Однако вы не можете просто сопоставить буквальные символы $
или \(
или \)
, поскольку каждый из них можетбыть предшествующим произвольному количеству \
обратной косой черты.Вместо этого токенизируют входную строку с обратными слешами, в долларах или скобках, и перебирают токены по порядку и отслеживают, что в последний раз соответствовало, чтобы определить их эффект (экранирование следующего символа, а также открывающая и закрывающая математические среды).
Вам не нужно продолжать синтаксический анализ, если вы находитесь за запрошенной позицией и вне раздела математической среды;у вас уже есть ответ, и вы можете вернуть False
рано.
Вот моя реализация такого синтаксического анализатора:
import re
_maths_pairs = {
# keys are opening characters, values matching closing characters
# each is a tuple of char (string), escaped (boolean)
('$', False): ('$', False),
('(', True): (')', True),
}
_tokens = re.compile(r'[\\$()]')
def _tokenize(s):
"""Generator that produces token, pos, prev_pos tuples for s
* token is a single character: a backslash, dollar or parethesis
* pos is the index into s for that token
* prev_pos is te position of the preceding token, or -1 if there
was no preceding token
"""
prev_pos = -1
for match in _tokens.finditer(s):
token, pos = match[0], match.start()
yield token, pos, prev_pos
prev_pos = pos
def is_maths(s, pos):
"""Determines if pos in s is within a LaTeX maths environment"""
expected_closer = None # (char, escaped) if within $...$ or \(...\)
opener_pos = None # position of last opener character
escaped = False # True if the most recent token was an escaping backslash
for token, token_pos, prev_pos in _tokenize(s):
if expected_closer is None and token_pos > pos:
# we are past the desired position, it'll never be within a
# maths environment.
return False
# if there was more text between the current token and the last
# backslash, then that backslash applied to something else.
if escaped and token_pos > prev_pos + 1:
escaped = False
if token == '\\':
# toggle the escaped flag; doubled escapes negate
escaped = not escaped
elif (token, escaped) == expected_closer:
if opener_pos < pos < token_pos:
# position is after the opener, before the closer
# so within a maths environment.
return True
expected_closer = None
elif expected_closer is None and (token, escaped) in _maths_pairs:
expected_closer = _maths_pairs[(token, escaped)]
opener_pos = token_pos
prev_pos = token_pos
return False
Демонстрация:
>>> cost = r'a) This costs \$1 but price goes as $x^2$ for \(x\) item(s).'
>>> is_maths(cost, 0) # should be False
False
>>> is_maths(cost, 16) # should be False, preceding $ is escaped
False
>>> is_maths(cost, 37) # should be True, within $...$
True
>>> is_maths(cost, 48) # should be True, within \(...\)
True
>>> is_maths(cost, 57) # should be False, within unescaped (...)
False
и дополнительныетесты, чтобы показать, что экранирование обрабатывается правильно:
>>> is_maths(r'Doubled escapes negate: \\$x^2$', 27) # should be true
True
>>> is_maths(r'Doubled escapes negate: \\(x\\)', 27) # no longer escaped, so false
False
Моя реализация старательно игнорирует искаженные проблемы LaTeX;неэкранированные $
символы в \(...\)
или экранированные \(
и \)
символы в $...$
игнорируются, как и другие \(
открыватели внутри \(...\)
последовательностей или \)
доводчики без соответствующего \(
открывалка предшествующая.Это гарантирует, что функция продолжает работать, даже если задан ввод, который сам LaTeX не будет отображать.Однако анализатор можно изменить, выдав исключение или вернув False
в этих случаях.В этом случае вам нужно добавить глобальный набор, созданный из _math_pairs.keys() | _math_pairs.values()
, и проверить (char, escaped)
против этого набора, когда expected_closer is not None and (token, escaped) != expected_closer
имеет значение false (обнаружение разделителей вложенных сред), и проверить для char == ')' and escaped and expected_closer is None
, чтобы обнаружить \)
ближе безПроблема сошника.