telegramify-markdown 1.2.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/PKG-INFO +30 -3
  2. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/README.md +29 -2
  3. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/pyproject.toml +1 -1
  4. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/__init__.py +13 -2
  5. telegramify_markdown-1.3.0/src/telegramify_markdown/config.py +105 -0
  6. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/converter.py +207 -81
  7. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/entity.py +5 -4
  8. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/mdv2.py +45 -51
  9. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/mermaid.py +23 -15
  10. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/pipeline.py +8 -8
  11. telegramify_markdown-1.3.0/src/telegramify_markdown/py.typed +0 -0
  12. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/rich.py +170 -49
  13. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/stream/draft.py +24 -5
  14. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/stream/edit.py +1 -1
  15. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/tests/test_converter.py +335 -0
  16. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/tests/test_entity.py +36 -1
  17. telegramify_markdown-1.3.0/tests/test_performance.py +75 -0
  18. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/tests/test_pipeline.py +67 -0
  19. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/tests/test_rich.py +184 -8
  20. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/tests/test_stream.py +24 -0
  21. telegramify_markdown-1.2.0/src/telegramify_markdown/config.py +0 -62
  22. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/LICENSE +0 -0
  23. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/code_file.py +0 -0
  24. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/content.py +0 -0
  25. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/latex_escape/__init__.py +0 -0
  26. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/latex_escape/const.py +0 -0
  27. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/latex_escape/helper.py +0 -0
  28. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/logger.py +0 -0
  29. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/stream/__init__.py +0 -0
  30. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/stream/core.py +0 -0
  31. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/type.py +0 -0
  32. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/src/telegramify_markdown/word_count.py +0 -0
  33. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/tests/exp1.md +0 -0
  34. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/tests/exp2.md +0 -0
  35. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/tests/send_showcase.py +0 -0
  36. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/tests/showcase_latex.py +0 -0
  37. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/tests/test_mdv2.py +0 -0
  38. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/tests/test_mermaid.py +0 -0
  39. {telegramify_markdown-1.2.0 → telegramify_markdown-1.3.0}/tests/test_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: telegramify-markdown
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Convert Markdown to Telegram plain text + MessageEntity pairs
5
5
  Author-Email: sudoskys <coldlando@hotmail.com>
6
6
  License: MIT
@@ -340,7 +340,7 @@ This handles all MarkdownV2 escaping rules correctly (different escaping for nor
340
340
 
341
341
  ## ⚙️ Configuration
342
342
 
343
- Customize heading symbols, link symbols, expandable citation behavior, and Mermaid rendering:
343
+ Customize heading symbols, link symbols, list markers, expandable citation behavior, and Mermaid rendering:
344
344
 
345
345
  ```python
346
346
  from telegramify_markdown.config import get_runtime_config
@@ -348,6 +348,8 @@ from telegramify_markdown.config import get_runtime_config
348
348
  cfg = get_runtime_config()
349
349
  cfg.markdown_symbol.heading_level_1 = "📌"
350
350
  cfg.markdown_symbol.link = "🔗"
351
+ cfg.markdown_symbol.unordered_list_item = "-" # default "⦁"; try "•", "*", …
352
+ cfg.markdown_symbol.ordered_list_suffix = "." # renders "1. item"; ")" gives "1) item"
351
353
  cfg.cite_expandable = True # Long quotes become expandable_blockquote
352
354
  cfg.mermaid.width = 1280
353
355
  cfg.mermaid.scale = 2
@@ -361,6 +363,31 @@ cfg.mermaid.image_type = "webp"
361
363
  # cfg.markdown_symbol.heading_level_4 = ""
362
364
  ```
363
365
 
366
+ List markers are plain text and carry no entity. Task list items keep using
367
+ `task_completed` / `task_uncompleted` and replace the list marker entirely.
368
+
369
+ `get_runtime_config()` returns a **process-global** config. Setting it once at
370
+ startup is the common case. It is shared mutable state, so a bot that varies
371
+ symbols per chat must not mutate it inside a handler — concurrent requests would
372
+ read each other's values. Use an independent config instead and pass it in:
373
+
374
+ ```python
375
+ from telegramify_markdown import RenderConfig, telegramify
376
+
377
+ cfg = RenderConfig.isolated() # library defaults, independent
378
+ # cfg = get_runtime_config().copy() # or: start from the current global values
379
+ cfg.markdown_symbol.unordered_list_item = "-"
380
+
381
+ boxes = await telegramify(markdown, config=cfg)
382
+ ```
383
+
384
+ `config=` is accepted by `convert()`, `convert_with_segments()`, `markdownify()`,
385
+ `standardize()`, and `telegramify()` (which also applies `cfg.mermaid` to
386
+ rendered diagrams). `richify()` and `telegramify_rich()` take no config —
387
+ Rich HTML emits structural tags and reads no symbols.
388
+
389
+ See [docs/prd/render-config.md](./docs/prd/render-config.md) for the full contract.
390
+
364
391
  `telegramify()` picks up Mermaid settings from the runtime config. The default Mermaid width is `1000`.
365
392
 
366
393
  ## 📖 API Reference
@@ -509,7 +536,7 @@ Returns the length of a string in UTF-16 code units (what Telegram uses for offs
509
536
  ## 🔨 Supported Markdown Features
510
537
 
511
538
  - [x] Headings (Levels 1-6: H1-H2 bold+underline, H3-H4 bold, H5-H6 italic; H1-H4 with emoji prefix)
512
- - [x] `**Bold**`, `*Italic*`, `~~Strikethrough~~`
539
+ - [x] `**Bold**`, `*Italic*`, `~~Strikethrough~~` (a single `~`, as in `~5 min`, stays literal)
513
540
  - [x] `||Spoiler||`
514
541
  - [x] `[Links](url)` and `![Images](url)`
515
542
  - [x] Telegram custom emoji `![emoji](tg://emoji?id=...)`
@@ -320,7 +320,7 @@ This handles all MarkdownV2 escaping rules correctly (different escaping for nor
320
320
 
321
321
  ## ⚙️ Configuration
322
322
 
323
- Customize heading symbols, link symbols, expandable citation behavior, and Mermaid rendering:
323
+ Customize heading symbols, link symbols, list markers, expandable citation behavior, and Mermaid rendering:
324
324
 
325
325
  ```python
326
326
  from telegramify_markdown.config import get_runtime_config
@@ -328,6 +328,8 @@ from telegramify_markdown.config import get_runtime_config
328
328
  cfg = get_runtime_config()
329
329
  cfg.markdown_symbol.heading_level_1 = "📌"
330
330
  cfg.markdown_symbol.link = "🔗"
331
+ cfg.markdown_symbol.unordered_list_item = "-" # default "⦁"; try "•", "*", …
332
+ cfg.markdown_symbol.ordered_list_suffix = "." # renders "1. item"; ")" gives "1) item"
331
333
  cfg.cite_expandable = True # Long quotes become expandable_blockquote
332
334
  cfg.mermaid.width = 1280
333
335
  cfg.mermaid.scale = 2
@@ -341,6 +343,31 @@ cfg.mermaid.image_type = "webp"
341
343
  # cfg.markdown_symbol.heading_level_4 = ""
342
344
  ```
343
345
 
346
+ List markers are plain text and carry no entity. Task list items keep using
347
+ `task_completed` / `task_uncompleted` and replace the list marker entirely.
348
+
349
+ `get_runtime_config()` returns a **process-global** config. Setting it once at
350
+ startup is the common case. It is shared mutable state, so a bot that varies
351
+ symbols per chat must not mutate it inside a handler — concurrent requests would
352
+ read each other's values. Use an independent config instead and pass it in:
353
+
354
+ ```python
355
+ from telegramify_markdown import RenderConfig, telegramify
356
+
357
+ cfg = RenderConfig.isolated() # library defaults, independent
358
+ # cfg = get_runtime_config().copy() # or: start from the current global values
359
+ cfg.markdown_symbol.unordered_list_item = "-"
360
+
361
+ boxes = await telegramify(markdown, config=cfg)
362
+ ```
363
+
364
+ `config=` is accepted by `convert()`, `convert_with_segments()`, `markdownify()`,
365
+ `standardize()`, and `telegramify()` (which also applies `cfg.mermaid` to
366
+ rendered diagrams). `richify()` and `telegramify_rich()` take no config —
367
+ Rich HTML emits structural tags and reads no symbols.
368
+
369
+ See [docs/prd/render-config.md](./docs/prd/render-config.md) for the full contract.
370
+
344
371
  `telegramify()` picks up Mermaid settings from the runtime config. The default Mermaid width is `1000`.
345
372
 
346
373
  ## 📖 API Reference
@@ -489,7 +516,7 @@ Returns the length of a string in UTF-16 code units (what Telegram uses for offs
489
516
  ## 🔨 Supported Markdown Features
490
517
 
491
518
  - [x] Headings (Levels 1-6: H1-H2 bold+underline, H3-H4 bold, H5-H6 italic; H1-H4 with emoji prefix)
492
- - [x] `**Bold**`, `*Italic*`, `~~Strikethrough~~`
519
+ - [x] `**Bold**`, `*Italic*`, `~~Strikethrough~~` (a single `~`, as in `~5 min`, stays literal)
493
520
  - [x] `||Spoiler||`
494
521
  - [x] `[Links](url)` and `![Images](url)`
495
522
  - [x] Telegram custom emoji `![emoji](tg://emoji?id=...)`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "telegramify-markdown"
3
- version = "1.2.0"
3
+ version = "1.3.0"
4
4
  description = "Convert Markdown to Telegram plain text + MessageEntity pairs"
5
5
  authors = [
6
6
  { name = "sudoskys", email = "coldlando@hotmail.com" },
@@ -6,6 +6,7 @@ import warnings
6
6
  from typing import Union
7
7
 
8
8
  from telegramify_markdown import config
9
+ from telegramify_markdown.config import RenderConfig
9
10
  from telegramify_markdown.converter import convert as convert
10
11
  from telegramify_markdown.entity import MessageEntity, split_entities, utf16_len
11
12
  from telegramify_markdown.content import ContentType, ContentTypes, ContentTrace, File, Photo, RichMessage, Text
@@ -23,6 +24,7 @@ __all__ = [
23
24
  "split_rich",
24
25
  "telegramify_rich",
25
26
  "config",
27
+ "RenderConfig",
26
28
  "MessageEntity",
27
29
  "InputRichMessage",
28
30
  "RichMessage",
@@ -43,6 +45,7 @@ def markdownify(
43
45
  max_line_length: int | None = None,
44
46
  normalize_whitespace: bool = False,
45
47
  latex_escape: bool = True,
48
+ config: RenderConfig | None = None,
46
49
  ) -> str:
47
50
  """Convert Markdown to a Telegram MarkdownV2 string.
48
51
 
@@ -51,6 +54,7 @@ def markdownify(
51
54
 
52
55
  :param max_line_length: Deprecated (ignored). Kept for 0.x compatibility.
53
56
  :param normalize_whitespace: Deprecated (ignored). Kept for 0.x compatibility.
57
+ :param config: Render configuration. Uses the global config if None.
54
58
  """
55
59
  if max_line_length is not None:
56
60
  warnings.warn(
@@ -66,7 +70,7 @@ def markdownify(
66
70
  DeprecationWarning,
67
71
  stacklevel=2,
68
72
  )
69
- return entities_to_markdownv2(*convert(content, latex_escape=latex_escape))
73
+ return entities_to_markdownv2(*convert(content, latex_escape=latex_escape, config=config))
70
74
 
71
75
 
72
76
  def standardize(
@@ -75,6 +79,7 @@ def standardize(
75
79
  max_line_length: int | None = None,
76
80
  normalize_whitespace: bool = False,
77
81
  latex_escape: bool = True,
82
+ config: RenderConfig | None = None,
78
83
  ) -> str:
79
84
  """Alias for :func:`markdownify`, kept for 0.x compatibility."""
80
85
  return markdownify(
@@ -82,6 +87,7 @@ def standardize(
82
87
  max_line_length=max_line_length,
83
88
  normalize_whitespace=normalize_whitespace,
84
89
  latex_escape=latex_escape,
90
+ config=config,
85
91
  )
86
92
 
87
93
 
@@ -95,6 +101,7 @@ async def telegramify(
95
101
  latex_escape: bool = True,
96
102
  render_mermaid: bool = True,
97
103
  min_file_lines: int = 1,
104
+ config: RenderConfig | None = None,
98
105
  ) -> list[Union[Text, File, Photo]]:
99
106
  """Convert markdown to Telegram-ready content segments.
100
107
 
@@ -110,6 +117,9 @@ async def telegramify(
110
117
  :param min_file_lines: Minimum line count for a code block to be extracted
111
118
  as a separate file. Set to ``0`` to disable file extraction entirely
112
119
  (all code blocks stay inline as ``pre`` entities).
120
+ :param config: Render configuration. Uses the global config if None.
121
+ Pass ``RenderConfig.isolated()`` to keep concurrent requests from
122
+ sharing symbol settings.
113
123
  :return: Ordered list of Text, File, or Photo objects ready for the Telegram Bot API.
114
124
  """
115
125
  if max_word_count is not None:
@@ -119,6 +129,7 @@ async def telegramify(
119
129
  DeprecationWarning,
120
130
  stacklevel=2,
121
131
  )
132
+ max_message_length = max_word_count
122
133
  if max_line_length is not None:
123
134
  warnings.warn(
124
135
  "max_line_length is deprecated and ignored in 1.x. "
@@ -133,7 +144,6 @@ async def telegramify(
133
144
  DeprecationWarning,
134
145
  stacklevel=2,
135
146
  )
136
- max_message_length = max_word_count
137
147
 
138
148
  from telegramify_markdown.pipeline import process_markdown
139
149
 
@@ -143,4 +153,5 @@ async def telegramify(
143
153
  latex_escape=latex_escape,
144
154
  render_mermaid=render_mermaid,
145
155
  min_file_lines=min_file_lines,
156
+ config=config,
146
157
  )
@@ -0,0 +1,105 @@
1
+ """Render configuration.
2
+
3
+ ``get_runtime_config()`` returns the process-global config; setting it once at
4
+ startup is the common case. When rendering must vary per session or per thread,
5
+ take an independent config with ``RenderConfig.isolated()`` and pass it to
6
+ ``convert(config=...)`` / ``telegramify(config=...)`` / ``markdownify(config=...)``.
7
+
8
+ The global instance is shared mutable state: writing to it inside a handler is
9
+ visible to every concurrent conversion that does not carry its own config.
10
+ pyTelegramBotAPI runs handlers on multiple threads by default, and on the
11
+ asyncio side "mutate global -> await -> render" always yields control in
12
+ between. Per-request configuration requires isolated().
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import copy
18
+
19
+
20
+ class Symbol:
21
+ def __init__(self):
22
+ self.heading_level_1: str = "\N{PUSHPIN}" # 📌
23
+ self.heading_level_2: str = "\N{PENCIL}" # ✏️
24
+ self.heading_level_3: str = "\N{BOOKS}" # 📚
25
+ self.heading_level_4: str = "\N{BOOKMARK}" # 🔖
26
+ self.heading_level_5: str = ""
27
+ self.heading_level_6: str = ""
28
+ self.image: str = "\N{FRAME WITH PICTURE}" # 🖼
29
+ self.link: str = "\N{LINK SYMBOL}" # 🔗
30
+ self.task_completed: str = "\N{WHITE HEAVY CHECK MARK}" # ✅
31
+ self.task_uncompleted: str = "\N{BALLOT BOX WITH CHECK}" # ☑️
32
+ self.horizontal_rule: str = "————————"
33
+ # List markers: written after the indent, before the item text.
34
+ # Always plain text, never covered by an entity.
35
+ # @see https://github.com/sudoskys/telegramify-markdown/issues/116
36
+ self.unordered_list_item: str = "\N{Z NOTATION SPOT}" # ⦁
37
+ self.ordered_list_suffix: str = "."
38
+
39
+
40
+ class Mermaid:
41
+ def __init__(self):
42
+ self.theme: str = "default"
43
+ self.width: int = 1000
44
+ self.scale: int = 2
45
+ self.image_type: str = "webp"
46
+
47
+
48
+ class RenderConfig:
49
+ """Render configuration.
50
+
51
+ Bare construction returns the global instance, equivalent to
52
+ ``get_runtime_config()`` and matching 1.x behaviour. For configs that do not
53
+ affect each other, use :meth:`isolated`.
54
+ """
55
+
56
+ _global: RenderConfig # created at import, below the class
57
+ _markdown_symbol: Symbol
58
+ _mermaid: Mermaid
59
+ _cite_expandable: bool
60
+
61
+ # No __init__ on purpose: repeated construction must not reset settings
62
+ # that have already been applied to the global instance.
63
+ def __new__(cls) -> RenderConfig:
64
+ return cls._global
65
+
66
+ def __reduce__(self) -> tuple:
67
+ # copy and pickle would otherwise rebuild through __new__, i.e. the global
68
+ return type(self).isolated, (), self.__dict__
69
+
70
+ @classmethod
71
+ def isolated(cls) -> RenderConfig:
72
+ """Build an independent config, sharing nothing with the global one."""
73
+ instance = super().__new__(cls)
74
+ instance._markdown_symbol = Symbol()
75
+ instance._mermaid = Mermaid()
76
+ instance._cite_expandable = True
77
+ return instance
78
+
79
+ def copy(self) -> RenderConfig:
80
+ """Copy this config into an independent one, symbol tables included."""
81
+ return copy.deepcopy(self)
82
+
83
+ @property
84
+ def markdown_symbol(self) -> Symbol:
85
+ return self._markdown_symbol
86
+
87
+ @property
88
+ def mermaid(self) -> Mermaid:
89
+ return self._mermaid
90
+
91
+ @property
92
+ def cite_expandable(self) -> bool:
93
+ return self._cite_expandable
94
+
95
+ @cite_expandable.setter
96
+ def cite_expandable(self, value: bool):
97
+ self._cite_expandable = value
98
+
99
+
100
+ RenderConfig._global = RenderConfig.isolated()
101
+
102
+
103
+ # Global accessor function for accessing the RenderConfig singleton
104
+ def get_runtime_config() -> RenderConfig:
105
+ return RenderConfig()