fix: add unescape for spans

This commit is contained in:
firedotguy
2026-03-01 00:06:16 +03:00
parent 86a378b613
commit 994a38e945
4 changed files with 97 additions and 8 deletions

0
' Normal file
View File

View File

@@ -92,6 +92,15 @@ from itd.utils import parse_html
print(с.create_post(*parse_html('значит, я это щас отправил со своего клиента, <b>воот</b>. И еще тут спаны написаны через html, по типу < i > <i>11</i>'))) print(с.create_post(*parse_html('значит, я это щас отправил со своего клиента, <b>воот</b>. И еще тут спаны написаны через html, по типу < i > <i>11</i>')))
``` ```
Поддерживаемые теги:
- `<b>`: жирный
- `<i>`: курсивный
- `<s>`: зачеркнутый
- `<u>`: подчеркнутый
- `<code>`: код
- `<spoiler>`: спойлер
- `<a href="https://google.com">`: ссылка
- `<q>`: цитата
<!-- ### SSE - прослушивание уведомлений в реальном времени <!-- ### SSE - прослушивание уведомлений в реальном времени

View File

@@ -59,6 +59,7 @@ class Span(BaseModel):
length: int length: int
offset: int offset: int
type: SpanType type: SpanType
url: str | None = None
class _PostCounts(TextObject): class _PostCounts(TextObject):

View File

@@ -5,46 +5,123 @@ from itd.enums import SpanType
class Tag: 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.open = open
self.close = close self.close = close
self.type = type self.type = type
self.url = url
def _parse_spans(text: str, tags: list[Tag]) -> tuple[str, list[Span]]: def _parse_spans(text: str, tags: list[Tag]) -> tuple[str, list[Span]]:
spans: 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] = [] clean_chars: list[str] = []
i = 0 i = 0
while i < len(text): while i < len(text):
# Проверка на экранирование
escaped = text[i] == '\\'
# Сначала проверяем закрывающие теги (с проверкой на экранирование)
closed = False closed = False
for idx, tag in enumerate(tags): for idx, tag in enumerate(tags):
if text.startswith(tag.close, i) and stack and stack[-1][0] == idx: if text.startswith(tag.close, i) and stack and stack[-1][0] == idx:
_, span_type, offset, _ = stack.pop() if escaped:
spans.append(Span(length=len(clean_chars) - offset, offset=offset, type=span_type)) # Экранированный закрывающий тег — выводим как текст (без слэша)
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) i += len(tag.close)
closed = True closed = True
break break
if closed: if closed:
continue continue
# Затем проверяем открывающие теги
opened = False opened = False
for idx, tag in enumerate(tags): for idx, tag in enumerate(tags):
if text.startswith(tag.open, i): if tag.type == SpanType.LINK:
stack.append((idx, tag.type, len(clean_chars), i)) 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) i += len(tag.open)
opened = True opened = True
break break
if opened: if opened:
continue 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]) clean_chars.append(text[i])
i += 1 i += 1
if stack: 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}') raise ValueError(f'No closing tag for {last_type.value} at pos {raw_pos}')
spans.sort(key=lambda span: span.offset) 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('<u>', '</u>', SpanType.UNDERLINE),
Tag('<code>', '</code>', SpanType.MONOSPACE), Tag('<code>', '</code>', SpanType.MONOSPACE),
Tag('<spoiler>', '</spoiler>', SpanType.SPOILER), Tag('<spoiler>', '</spoiler>', SpanType.SPOILER),
Tag(r'<a href="([^"]+)">', '</a>', SpanType.LINK),
Tag(r'<q>', '</q>', SpanType.QUOTE),
], ],
) )