notionary 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

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 (51) hide show
  1. notionary/__init__.py +9 -0
  2. notionary/core/__init__.py +0 -0
  3. notionary/core/converters/__init__.py +50 -0
  4. notionary/core/converters/elements/__init__.py +0 -0
  5. notionary/core/converters/elements/bookmark_element.py +224 -0
  6. notionary/core/converters/elements/callout_element.py +179 -0
  7. notionary/core/converters/elements/code_block_element.py +153 -0
  8. notionary/core/converters/elements/column_element.py +294 -0
  9. notionary/core/converters/elements/divider_element.py +73 -0
  10. notionary/core/converters/elements/heading_element.py +84 -0
  11. notionary/core/converters/elements/image_element.py +130 -0
  12. notionary/core/converters/elements/list_element.py +130 -0
  13. notionary/core/converters/elements/notion_block_element.py +51 -0
  14. notionary/core/converters/elements/paragraph_element.py +73 -0
  15. notionary/core/converters/elements/qoute_element.py +242 -0
  16. notionary/core/converters/elements/table_element.py +306 -0
  17. notionary/core/converters/elements/text_inline_formatter.py +294 -0
  18. notionary/core/converters/elements/todo_lists.py +114 -0
  19. notionary/core/converters/elements/toggle_element.py +205 -0
  20. notionary/core/converters/elements/video_element.py +159 -0
  21. notionary/core/converters/markdown_to_notion_converter.py +482 -0
  22. notionary/core/converters/notion_to_markdown_converter.py +45 -0
  23. notionary/core/converters/registry/__init__.py +0 -0
  24. notionary/core/converters/registry/block_element_registry.py +234 -0
  25. notionary/core/converters/registry/block_element_registry_builder.py +280 -0
  26. notionary/core/database/database_info_service.py +43 -0
  27. notionary/core/database/database_query_service.py +73 -0
  28. notionary/core/database/database_schema_service.py +57 -0
  29. notionary/core/database/models/page_result.py +10 -0
  30. notionary/core/database/notion_database_manager.py +332 -0
  31. notionary/core/database/notion_database_manager_factory.py +233 -0
  32. notionary/core/database/notion_database_schema.py +415 -0
  33. notionary/core/database/notion_database_writer.py +390 -0
  34. notionary/core/database/page_service.py +161 -0
  35. notionary/core/notion_client.py +134 -0
  36. notionary/core/page/meta_data/metadata_editor.py +37 -0
  37. notionary/core/page/notion_page_manager.py +110 -0
  38. notionary/core/page/page_content_manager.py +85 -0
  39. notionary/core/page/property_formatter.py +97 -0
  40. notionary/exceptions/database_exceptions.py +76 -0
  41. notionary/exceptions/page_creation_exception.py +9 -0
  42. notionary/util/logging_mixin.py +47 -0
  43. notionary/util/singleton_decorator.py +20 -0
  44. notionary/util/uuid_utils.py +24 -0
  45. {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/METADATA +1 -1
  46. notionary-0.1.3.dist-info/RECORD +49 -0
  47. notionary-0.1.3.dist-info/top_level.txt +1 -0
  48. notionary-0.1.1.dist-info/RECORD +0 -5
  49. notionary-0.1.1.dist-info/top_level.txt +0 -1
  50. {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/WHEEL +0 -0
  51. {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,306 @@
1
+ # File: elements/tables.py
2
+
3
+ from typing import Dict, Any, Optional, List, Tuple
4
+ from typing_extensions import override
5
+ import re
6
+ from notionary.core.converters.elements.notion_block_element import NotionBlockElement
7
+ from notionary.core.converters.elements.text_inline_formatter import TextInlineFormatter
8
+
9
+
10
+ class TableElement(NotionBlockElement):
11
+ """
12
+ Handles conversion between Markdown tables and Notion table blocks.
13
+
14
+ Markdown table syntax:
15
+ | Header 1 | Header 2 | Header 3 |
16
+ | -------- | -------- | -------- |
17
+ | Cell 1 | Cell 2 | Cell 3 |
18
+ | Cell 4 | Cell 5 | Cell 6 |
19
+
20
+ The second line with dashes and optional colons defines column alignment.
21
+ """
22
+
23
+ # Patterns for detecting Markdown tables
24
+ ROW_PATTERN = re.compile(r"^\s*\|(.+)\|\s*$")
25
+ SEPARATOR_PATTERN = re.compile(r"^\s*\|([\s\-:|]+)\|\s*$")
26
+
27
+ @override
28
+ @staticmethod
29
+ def match_markdown(text: str) -> bool:
30
+ """Check if text contains a markdown table."""
31
+ lines = text.split("\n")
32
+
33
+ if len(lines) < 3:
34
+ return False
35
+
36
+ for i, line in enumerate(lines[:-2]):
37
+ if (
38
+ TableElement.ROW_PATTERN.match(line)
39
+ and TableElement.SEPARATOR_PATTERN.match(lines[i + 1])
40
+ and TableElement.ROW_PATTERN.match(lines[i + 2])
41
+ ):
42
+ return True
43
+
44
+ return False
45
+
46
+ @override
47
+ @staticmethod
48
+ def match_notion(block: Dict[str, Any]) -> bool:
49
+ """Check if block is a Notion table."""
50
+ return block.get("type") == "table"
51
+
52
+ @override
53
+ @staticmethod
54
+ def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
55
+ """Convert markdown table to Notion table block."""
56
+ if not TableElement.match_markdown(text):
57
+ return None
58
+
59
+ lines = text.split("\n")
60
+
61
+ table_start = TableElement._find_table_start(lines)
62
+ if table_start is None:
63
+ return None
64
+
65
+ table_end = TableElement._find_table_end(lines, table_start)
66
+ table_lines = lines[table_start:table_end]
67
+
68
+ rows = TableElement._extract_table_rows(table_lines)
69
+ if not rows:
70
+ return None
71
+
72
+ column_count = len(rows[0])
73
+ TableElement._normalize_row_lengths(rows, column_count)
74
+
75
+ return {
76
+ "type": "table",
77
+ "table": {
78
+ "table_width": column_count,
79
+ "has_column_header": True,
80
+ "has_row_header": False,
81
+ "children": TableElement._create_table_rows(rows),
82
+ },
83
+ }
84
+
85
+ @override
86
+ @staticmethod
87
+ def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
88
+ """Convert Notion table block to markdown table."""
89
+ if block.get("type") != "table":
90
+ return None
91
+
92
+ table_data = block.get("table", {})
93
+ children = block.get("children", [])
94
+
95
+ if not children:
96
+ table_width = table_data.get("table_width", 3)
97
+
98
+ header = (
99
+ "| " + " | ".join([f"Column {i+1}" for i in range(table_width)]) + " |"
100
+ )
101
+ separator = (
102
+ "| " + " | ".join(["--------" for _ in range(table_width)]) + " |"
103
+ )
104
+ data_row = (
105
+ "| " + " | ".join([" " for _ in range(table_width)]) + " |"
106
+ )
107
+
108
+ table_rows = [header, separator, data_row]
109
+ return "\n".join(table_rows)
110
+
111
+ table_rows = []
112
+ header_processed = False
113
+
114
+ for child in children:
115
+ if child.get("type") != "table_row":
116
+ continue
117
+
118
+ row_data = child.get("table_row", {})
119
+ cells = row_data.get("cells", [])
120
+
121
+ row_cells = []
122
+ for cell in cells:
123
+ cell_text = TextInlineFormatter.extract_text_with_formatting(cell)
124
+ row_cells.append(cell_text or "")
125
+
126
+ row = "| " + " | ".join(row_cells) + " |"
127
+ table_rows.append(row)
128
+
129
+ if not header_processed and table_data.get("has_column_header", True):
130
+ header_processed = True
131
+ separator = (
132
+ "| " + " | ".join(["--------" for _ in range(len(cells))]) + " |"
133
+ )
134
+ table_rows.append(separator)
135
+
136
+ if not table_rows:
137
+ return None
138
+
139
+ if len(table_rows) == 1 and table_data.get("has_column_header", True):
140
+ cells_count = len(children[0].get("table_row", {}).get("cells", []))
141
+ separator = (
142
+ "| " + " | ".join(["--------" for _ in range(cells_count)]) + " |"
143
+ )
144
+ table_rows.insert(1, separator)
145
+
146
+ return "\n".join(table_rows)
147
+
148
+ @override
149
+ @staticmethod
150
+ def is_multiline() -> bool:
151
+ """Indicates if this element handles content that spans multiple lines."""
152
+ return True
153
+
154
+ @staticmethod
155
+ def _find_table_start(lines: List[str]) -> Optional[int]:
156
+ """Find the start index of a table in the lines."""
157
+ for i in range(len(lines) - 2):
158
+ if (
159
+ TableElement.ROW_PATTERN.match(lines[i])
160
+ and TableElement.SEPARATOR_PATTERN.match(lines[i + 1])
161
+ and TableElement.ROW_PATTERN.match(lines[i + 2])
162
+ ):
163
+ return i
164
+ return None
165
+
166
+ @staticmethod
167
+ def _find_table_end(lines: List[str], start_idx: int) -> int:
168
+ """Find the end index of a table, starting from start_idx."""
169
+ end_idx = start_idx + 3 # Minimum: Header, Separator, one data row
170
+ while end_idx < len(lines) and TableElement.ROW_PATTERN.match(lines[end_idx]):
171
+ end_idx += 1
172
+ return end_idx
173
+
174
+ @staticmethod
175
+ def _extract_table_rows(table_lines: List[str]) -> List[List[str]]:
176
+ """Extract row contents from table lines, excluding separator line."""
177
+ rows = []
178
+ for i, line in enumerate(table_lines):
179
+ if i != 1 and TableElement.ROW_PATTERN.match(line): # Skip separator line
180
+ cells = TableElement._parse_table_row(line)
181
+ if cells:
182
+ rows.append(cells)
183
+ return rows
184
+
185
+ @staticmethod
186
+ def _normalize_row_lengths(rows: List[List[str]], column_count: int) -> None:
187
+ """Normalize row lengths to the specified column count."""
188
+ for row in rows:
189
+ if len(row) < column_count:
190
+ row.extend([""] * (column_count - len(row)))
191
+ elif len(row) > column_count:
192
+ del row[column_count:]
193
+
194
+ @staticmethod
195
+ def _parse_table_row(row_text: str) -> List[str]:
196
+ """Convert table row text to cell contents."""
197
+ row_content = row_text.strip()
198
+
199
+ if row_content.startswith("|"):
200
+ row_content = row_content[1:]
201
+ if row_content.endswith("|"):
202
+ row_content = row_content[:-1]
203
+
204
+ return [cell.strip() for cell in row_content.split("|")]
205
+
206
+ @staticmethod
207
+ def _create_table_rows(rows: List[List[str]]) -> List[Dict[str, Any]]:
208
+ """Create Notion table rows from cell contents."""
209
+ table_rows = []
210
+
211
+ for row in rows:
212
+ cells_data = []
213
+
214
+ for cell_content in row:
215
+ rich_text = TextInlineFormatter.parse_inline_formatting(cell_content)
216
+
217
+ if not rich_text:
218
+ rich_text = [
219
+ {
220
+ "type": "text",
221
+ "text": {"content": ""},
222
+ "annotations": {
223
+ "bold": False,
224
+ "italic": False,
225
+ "strikethrough": False,
226
+ "underline": False,
227
+ "code": False,
228
+ "color": "default",
229
+ },
230
+ "plain_text": "",
231
+ "href": None,
232
+ }
233
+ ]
234
+
235
+ cells_data.append(rich_text)
236
+
237
+ table_rows.append({"type": "table_row", "table_row": {"cells": cells_data}})
238
+
239
+ return table_rows
240
+
241
+ @staticmethod
242
+ def find_matches(text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
243
+ """
244
+ Find all tables in the text and return their positions.
245
+
246
+ Args:
247
+ text: The text to search in
248
+
249
+ Returns:
250
+ List of tuples with (start_pos, end_pos, block)
251
+ """
252
+ matches = []
253
+ lines = text.split("\n")
254
+
255
+ i = 0
256
+ while i < len(lines) - 2:
257
+ if (
258
+ TableElement.ROW_PATTERN.match(lines[i])
259
+ and TableElement.SEPARATOR_PATTERN.match(lines[i + 1])
260
+ and TableElement.ROW_PATTERN.match(lines[i + 2])
261
+ ):
262
+
263
+ start_line = i
264
+ end_line = TableElement._find_table_end(lines, start_line)
265
+
266
+ start_pos = TableElement._calculate_position(lines, 0, start_line)
267
+ end_pos = start_pos + TableElement._calculate_position(
268
+ lines, start_line, end_line
269
+ )
270
+
271
+ table_text = "\n".join(lines[start_line:end_line])
272
+ table_block = TableElement.markdown_to_notion(table_text)
273
+
274
+ if table_block:
275
+ matches.append((start_pos, end_pos, table_block))
276
+
277
+ i = end_line
278
+ else:
279
+ i += 1
280
+
281
+ return matches
282
+
283
+ @staticmethod
284
+ def _calculate_position(lines: List[str], start: int, end: int) -> int:
285
+ """Calculate the text position in characters from line start to end."""
286
+ position = 0
287
+ for i in range(start, end):
288
+ position += len(lines[i]) + 1 # +1 for newline
289
+ return position
290
+
291
+ @classmethod
292
+ def get_llm_prompt_content(cls) -> dict:
293
+ """Returns information for LLM prompts about this element."""
294
+ return {
295
+ "description": "Creates formatted tables with rows and columns for structured data.",
296
+ "when_to_use": "Use tables to organize and present structured data in a grid format, making information easier to compare and analyze. Tables are ideal for data sets, comparison charts, pricing information, or any content that benefits from columnar organization.",
297
+ "notes": [
298
+ "The header row is required and will be displayed differently in Notion",
299
+ "The separator row with dashes is required to define the table structure",
300
+ "Table cells support inline formatting such as **bold** and *italic*",
301
+ ],
302
+ "examples": [
303
+ "| Product | Price | Stock |\n| ------- | ----- | ----- |\n| Widget A | $10.99 | 42 |\n| Widget B | $14.99 | 27 |",
304
+ "| Name | Role | Department |\n| ---- | ---- | ---------- |\n| John Smith | Manager | Marketing |\n| Jane Doe | Director | Sales |",
305
+ ],
306
+ }
@@ -0,0 +1,294 @@
1
+ from typing import Dict, Any, List, Tuple
2
+ import re
3
+
4
+
5
+ class TextInlineFormatter:
6
+ """
7
+ Handles conversion between Markdown inline formatting and Notion rich text elements.
8
+
9
+ Supports various formatting options:
10
+ - Bold: **text**
11
+ - Italic: *text* or _text_
12
+ - Underline: __text__
13
+ - Strikethrough: ~~text~~
14
+ - Code: `text`
15
+ - Links: [text](url)
16
+ - Highlights: ==text== (default yellow) or ==color:text== (custom color)
17
+ """
18
+
19
+ # Format patterns for matching Markdown formatting
20
+ FORMAT_PATTERNS = [
21
+ (r"\*\*(.+?)\*\*", {"bold": True}),
22
+ (r"\*(.+?)\*", {"italic": True}),
23
+ (r"_(.+?)_", {"italic": True}),
24
+ (r"__(.+?)__", {"underline": True}),
25
+ (r"~~(.+?)~~", {"strikethrough": True}),
26
+ (r"`(.+?)`", {"code": True}),
27
+ (r"\[(.+?)\]\((.+?)\)", {"link": True}),
28
+ (r"==([a-z_]+):(.+?)==", {"highlight": True}),
29
+ (r"==(.+?)==", {"highlight_default": True}),
30
+ ]
31
+
32
+ # Valid colors for highlighting
33
+ VALID_COLORS = [
34
+ "default",
35
+ "gray",
36
+ "brown",
37
+ "orange",
38
+ "yellow",
39
+ "green",
40
+ "blue",
41
+ "purple",
42
+ "pink",
43
+ "red",
44
+ "gray_background",
45
+ "brown_background",
46
+ "orange_background",
47
+ "yellow_background",
48
+ "green_background",
49
+ "blue_background",
50
+ "purple_background",
51
+ "pink_background",
52
+ "red_background",
53
+ ]
54
+
55
+ @classmethod
56
+ def parse_inline_formatting(cls, text: str) -> List[Dict[str, Any]]:
57
+ """
58
+ Parse inline text formatting into Notion rich_text format.
59
+
60
+ Args:
61
+ text: Markdown text with inline formatting
62
+
63
+ Returns:
64
+ List of Notion rich_text objects
65
+ """
66
+ if not text:
67
+ return []
68
+
69
+ return cls._split_text_into_segments(text, cls.FORMAT_PATTERNS)
70
+
71
+ @classmethod
72
+ def _split_text_into_segments(
73
+ cls, text: str, format_patterns: List[Tuple]
74
+ ) -> List[Dict[str, Any]]:
75
+ """
76
+ Split text into segments by formatting markers and convert to Notion rich_text format.
77
+
78
+ Args:
79
+ text: Text to split
80
+ format_patterns: List of (regex pattern, formatting dict) tuples
81
+
82
+ Returns:
83
+ List of Notion rich_text objects
84
+ """
85
+ segments = []
86
+ remaining_text = text
87
+
88
+ while remaining_text:
89
+ earliest_match = None
90
+ earliest_format = None
91
+ earliest_pos = len(remaining_text)
92
+
93
+ # Find the earliest formatting marker
94
+ for pattern, formatting in format_patterns:
95
+ match = re.search(pattern, remaining_text)
96
+ if match and match.start() < earliest_pos:
97
+ earliest_match = match
98
+ earliest_format = formatting
99
+ earliest_pos = match.start()
100
+
101
+ if earliest_match is None:
102
+ if remaining_text:
103
+ segments.append(cls._create_text_element(remaining_text, {}))
104
+ break
105
+
106
+ if earliest_pos > 0:
107
+ segments.append(
108
+ cls._create_text_element(remaining_text[:earliest_pos], {})
109
+ )
110
+
111
+ if "highlight" in earliest_format:
112
+ color = earliest_match.group(1)
113
+ content = earliest_match.group(2)
114
+
115
+ if color not in cls.VALID_COLORS:
116
+ if not color.endswith("_background"):
117
+ color = f"{color}_background"
118
+
119
+ if color not in cls.VALID_COLORS:
120
+ color = "yellow_background"
121
+
122
+ segments.append(cls._create_text_element(content, {"color": color}))
123
+
124
+ elif "highlight_default" in earliest_format:
125
+ content = earliest_match.group(1)
126
+ segments.append(
127
+ cls._create_text_element(content, {"color": "yellow_background"})
128
+ )
129
+
130
+ elif "link" in earliest_format:
131
+ content = earliest_match.group(1)
132
+ url = earliest_match.group(2)
133
+ segments.append(cls._create_link_element(content, url))
134
+
135
+ else:
136
+ content = earliest_match.group(1)
137
+ segments.append(cls._create_text_element(content, earliest_format))
138
+
139
+ # Move past the processed segment
140
+ remaining_text = remaining_text[
141
+ earliest_pos + len(earliest_match.group(0)) :
142
+ ]
143
+
144
+ return segments
145
+
146
+ @classmethod
147
+ def _create_text_element(
148
+ cls, text: str, formatting: Dict[str, Any]
149
+ ) -> Dict[str, Any]:
150
+ """
151
+ Create a Notion text element with formatting.
152
+
153
+ Args:
154
+ text: The text content
155
+ formatting: Dictionary of formatting options
156
+
157
+ Returns:
158
+ Notion rich_text element
159
+ """
160
+ annotations = cls._default_annotations()
161
+
162
+ # Apply formatting
163
+ for key, value in formatting.items():
164
+ if key == "color":
165
+ annotations["color"] = value
166
+ elif key in annotations:
167
+ annotations[key] = value
168
+
169
+ return {
170
+ "type": "text",
171
+ "text": {"content": text},
172
+ "annotations": annotations,
173
+ "plain_text": text,
174
+ }
175
+
176
+ @classmethod
177
+ def _create_link_element(cls, text: str, url: str) -> Dict[str, Any]:
178
+ """
179
+ Create a Notion link element.
180
+
181
+ Args:
182
+ text: The link text
183
+ url: The URL
184
+
185
+ Returns:
186
+ Notion rich_text element with link
187
+ """
188
+ return {
189
+ "type": "text",
190
+ "text": {"content": text, "link": {"url": url}},
191
+ "annotations": cls._default_annotations(),
192
+ "plain_text": text,
193
+ }
194
+
195
+ @classmethod
196
+ def extract_text_with_formatting(cls, rich_text: List[Dict[str, Any]]) -> str:
197
+ """
198
+ Convert Notion rich_text elements back to Markdown formatted text.
199
+
200
+ Args:
201
+ rich_text: List of Notion rich_text elements
202
+
203
+ Returns:
204
+ Markdown formatted text
205
+ """
206
+ formatted_parts = []
207
+
208
+ for text_obj in rich_text:
209
+ # Fallback: If plain_text is missing, use text['content']
210
+ content = text_obj.get("plain_text")
211
+ if content is None:
212
+ content = text_obj.get("text", {}).get("content", "")
213
+
214
+ annotations = text_obj.get("annotations", {})
215
+
216
+ if annotations.get("code", False):
217
+ content = f"`{content}`"
218
+ if annotations.get("strikethrough", False):
219
+ content = f"~~{content}~~"
220
+ if annotations.get("underline", False):
221
+ content = f"__{content}__"
222
+ if annotations.get("italic", False):
223
+ content = f"*{content}*"
224
+ if annotations.get("bold", False):
225
+ content = f"**{content}**"
226
+
227
+ color = annotations.get("color", "default")
228
+ if color != "default":
229
+ content = f"=={color.replace('_background', '')}:{content}=="
230
+
231
+ text_data = text_obj.get("text", {})
232
+ link_data = text_data.get("link")
233
+ if link_data:
234
+ url = link_data.get("url", "")
235
+ content = f"[{content}]({url})"
236
+
237
+ formatted_parts.append(content)
238
+
239
+ return "".join(formatted_parts)
240
+
241
+ @classmethod
242
+ def _default_annotations(cls) -> Dict[str, bool]:
243
+ """
244
+ Create default annotations object.
245
+
246
+ Returns:
247
+ Default Notion text annotations
248
+ """
249
+ return {
250
+ "bold": False,
251
+ "italic": False,
252
+ "strikethrough": False,
253
+ "underline": False,
254
+ "code": False,
255
+ "color": "default",
256
+ }
257
+
258
+ @classmethod
259
+ def get_llm_prompt_content(cls) -> Dict[str, Any]:
260
+ """
261
+ Returns information about inline text formatting capabilities for LLM prompts.
262
+
263
+ This method provides documentation about supported inline formatting options
264
+ that can be used across all block elements.
265
+
266
+ Returns:
267
+ A dictionary with descriptions, syntax examples, and usage guidelines
268
+ """
269
+ return {
270
+ "description": "Standard Markdown formatting is supported in all text blocks. Additionally, a custom highlight syntax is available for emphasizing important information. To create vertical spacing between elements, use the special spacer tag.",
271
+ "syntax": [
272
+ "**text** - Bold text",
273
+ "*text* or _text_ - Italic text",
274
+ "__text__ - Underlined text",
275
+ "~~text~~ - Strikethrough text",
276
+ "`text` - Inline code",
277
+ "[text](url) - Link",
278
+ "==text== - Default highlight (yellow background)",
279
+ "==color:text== - Colored highlight (e.g., ==red:warning==)",
280
+ "<!-- spacer --> - Creates vertical spacing between elements",
281
+ ],
282
+ "examples": [
283
+ "This is a **bold** statement with some *italic* words.",
284
+ "This feature is ~~deprecated~~ as of version 2.0.",
285
+ "Edit the `config.json` file to configure settings.",
286
+ "Check our [documentation](https://docs.example.com) for more details.",
287
+ "==This is an important note== that you should remember.",
288
+ "==red:Warning:== This action cannot be undone.",
289
+ "==blue:Note:== Common colors include red, blue, green, yellow, purple.",
290
+ "First paragraph content.\n\n<!-- spacer -->\n\nSecond paragraph with additional spacing above.",
291
+ ],
292
+ "highlight_usage": "The highlight syntax (==text== and ==color:text==) should be used to emphasize important information, warnings, notes, or other content that needs to stand out. This is particularly useful for making content more scannable at a glance.",
293
+ "spacer_usage": "Use the <!-- spacer --> tag on its own line to create additional vertical spacing between elements. This is useful for improving readability by visually separating sections of content. Multiple spacer tags can be used for greater spacing.",
294
+ }