fix: add unescape for spans
This commit is contained in:
95
itd/utils.py
95
itd/utils.py
@@ -5,46 +5,123 @@ from itd.enums import SpanType
|
||||
|
||||
|
||||
class Tag:
|
||||
def __init__(self, open: str, close: str, type: SpanType, url: str | None = None):
|
||||
def __init__(self, open: str, close: str, type: SpanType):
|
||||
self.open = open
|
||||
self.close = close
|
||||
self.type = type
|
||||
self.url = url
|
||||
|
||||
|
||||
def _parse_spans(text: str, tags: list[Tag]) -> tuple[str, list[Span]]:
|
||||
spans: list[Span] = []
|
||||
stack: list[tuple[int, SpanType, int, int]] = []
|
||||
stack: list[tuple[int, SpanType, int, int, str | None]] = []
|
||||
clean_chars: list[str] = []
|
||||
i = 0
|
||||
|
||||
while i < len(text):
|
||||
# Проверка на экранирование
|
||||
escaped = text[i] == '\\'
|
||||
|
||||
# Сначала проверяем закрывающие теги (с проверкой на экранирование)
|
||||
closed = False
|
||||
for idx, tag in enumerate(tags):
|
||||
if text.startswith(tag.close, i) and stack and stack[-1][0] == idx:
|
||||
_, span_type, offset, _ = stack.pop()
|
||||
spans.append(Span(length=len(clean_chars) - offset, offset=offset, type=span_type))
|
||||
if escaped:
|
||||
# Экранированный закрывающий тег — выводим как текст (без слэша)
|
||||
clean_chars.append(tag.close)
|
||||
i += len(tag.close)
|
||||
closed = True
|
||||
break
|
||||
_, span_type, offset, _, url = stack.pop()
|
||||
spans.append(Span(length=len(clean_chars) - offset, offset=offset, type=span_type, url=url))
|
||||
i += len(tag.close)
|
||||
closed = True
|
||||
break
|
||||
if closed:
|
||||
continue
|
||||
|
||||
# Затем проверяем открывающие теги
|
||||
opened = False
|
||||
for idx, tag in enumerate(tags):
|
||||
if text.startswith(tag.open, i):
|
||||
stack.append((idx, tag.type, len(clean_chars), i))
|
||||
if tag.type == SpanType.LINK:
|
||||
if escaped:
|
||||
match = re.match(tag.open, text[i+1:])
|
||||
if match:
|
||||
# Экранированный открывающий тег — выводим как текст (без слэша)
|
||||
clean_chars.append(match.group(0))
|
||||
i += 1 + match.end()
|
||||
opened = True
|
||||
break
|
||||
else:
|
||||
match = re.match(tag.open, text[i:])
|
||||
if match:
|
||||
url = match.group(1) if match.groups() else None
|
||||
stack.append((idx, tag.type, len(clean_chars), i, url))
|
||||
i += match.end()
|
||||
opened = True
|
||||
break
|
||||
elif text.startswith(tag.open, i):
|
||||
if escaped:
|
||||
# Экранированный обычный тег — пропускаем, будет обработан в блоке is_escape
|
||||
break
|
||||
stack.append((idx, tag.type, len(clean_chars), i, None))
|
||||
i += len(tag.open)
|
||||
opened = True
|
||||
break
|
||||
if opened:
|
||||
continue
|
||||
|
||||
# Если это слэш, проверяем, не экранирует ли он следующий тег
|
||||
if escaped:
|
||||
# Проверяем, следует ли за слэшем тег
|
||||
is_escape = False
|
||||
for tag in tags:
|
||||
if tag.type == SpanType.LINK:
|
||||
if re.match(tag.open, text[i+1:]):
|
||||
is_escape = True
|
||||
break
|
||||
elif text.startswith(tag.open, i+1):
|
||||
is_escape = True
|
||||
break
|
||||
# Проверяем закрывающие теги
|
||||
if not is_escape:
|
||||
for tag in tags:
|
||||
if text.startswith(tag.close, i+1):
|
||||
is_escape = True
|
||||
break
|
||||
|
||||
if is_escape:
|
||||
# Пропускаем слэш и выводим следующий тег как текст
|
||||
i += 1
|
||||
# Находим и выводим экранированный тег
|
||||
skip = False
|
||||
for tag in tags:
|
||||
if tag.type == SpanType.LINK:
|
||||
match = re.match(tag.open, text[i:])
|
||||
if match:
|
||||
clean_chars.append(match.group(0))
|
||||
i += match.end()
|
||||
skip = True
|
||||
break
|
||||
elif text.startswith(tag.open, i):
|
||||
clean_chars.append(tag.open)
|
||||
i += len(tag.open)
|
||||
skip = True
|
||||
break
|
||||
if not skip:
|
||||
# Проверяем закрывающие теги
|
||||
for tag in tags:
|
||||
if text.startswith(tag.close, i):
|
||||
clean_chars.append(tag.close)
|
||||
i += len(tag.close)
|
||||
skip = True
|
||||
break
|
||||
continue
|
||||
|
||||
clean_chars.append(text[i])
|
||||
i += 1
|
||||
|
||||
if stack:
|
||||
_, last_type, _, raw_pos = stack[-1]
|
||||
_, last_type, _, raw_pos, _ = stack[-1]
|
||||
raise ValueError(f'No closing tag for {last_type.value} at pos {raw_pos}')
|
||||
|
||||
spans.sort(key=lambda span: span.offset)
|
||||
@@ -61,6 +138,8 @@ def parse_html(text: str) -> tuple[str, list[Span]]:
|
||||
Tag('<u>', '</u>', SpanType.UNDERLINE),
|
||||
Tag('<code>', '</code>', SpanType.MONOSPACE),
|
||||
Tag('<spoiler>', '</spoiler>', SpanType.SPOILER),
|
||||
Tag(r'<a href="([^"]+)">', '</a>', SpanType.LINK),
|
||||
Tag(r'<q>', '</q>', SpanType.QUOTE),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user