fix: add unescape for spans
This commit is contained in:
@@ -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 - прослушивание уведомлений в реальном времени
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
95
itd/utils.py
95
itd/utils.py
@@ -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),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user