dataops-sdk 0.3.0__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.

Potentially problematic release.


This version of dataops-sdk might be problematic. Click here for more details.

Files changed (79) hide show
  1. dataops_sdk/__init__.py +84 -0
  2. dataops_sdk/constants.py +6 -0
  3. dataops_sdk/exceptions.py +31 -0
  4. dataops_sdk/runner/__init__.py +46 -0
  5. dataops_sdk/runner/datasource_factory.py +83 -0
  6. dataops_sdk/runner/schemas/nacos/__init__.py +5 -0
  7. dataops_sdk/runner/schemas/nacos/config.py +40 -0
  8. dataops_sdk/runner/schemas/nacos/constants.py +235 -0
  9. dataops_sdk/runner/schemas/nacos/instance.py +103 -0
  10. dataops_sdk/runner/schemas/nacos/nacos_exception.py +44 -0
  11. dataops_sdk/runner/schemas/nacos/naming_param.py +62 -0
  12. dataops_sdk/runner/schemas/nacos/naming_request.py +84 -0
  13. dataops_sdk/runner/schemas/nacos/naming_response.py +50 -0
  14. dataops_sdk/runner/schemas/nacos/preserved_metadata_key.py +10 -0
  15. dataops_sdk/runner/schemas/nacos/service.py +119 -0
  16. dataops_sdk/runner/utils/__init__.py +42 -0
  17. dataops_sdk/runner/utils/common.py +209 -0
  18. dataops_sdk/runner/utils/dbConn_base.py +256 -0
  19. dataops_sdk/runner/utils/dbConn_sync.py +485 -0
  20. dataops_sdk/runner/utils/duckdb_manager.py +203 -0
  21. dataops_sdk/runner/utils/file_transfer_base.py +307 -0
  22. dataops_sdk/runner/utils/ftp_file_transfer.py +242 -0
  23. dataops_sdk/runner/utils/log.py +50 -0
  24. dataops_sdk/runner/utils/nacos.py +96 -0
  25. dataops_sdk/runner/utils/neo4j_sync.py +236 -0
  26. dataops_sdk/runner/utils/queue_base.py +224 -0
  27. dataops_sdk/runner/utils/queue_kafka.py +150 -0
  28. dataops_sdk/runner/utils/sftp_file_transfer.py +243 -0
  29. dataops_sdk/schemas/__init__.py +32 -0
  30. dataops_sdk/schemas/alarm.py +38 -0
  31. dataops_sdk/schemas/api.py +37 -0
  32. dataops_sdk/schemas/data_table.py +437 -0
  33. dataops_sdk/schemas/datasource.py +26 -0
  34. dataops_sdk/schemas/dict_.py +13 -0
  35. dataops_sdk/schemas/enums.py +120 -0
  36. dataops_sdk/schemas/group.py +40 -0
  37. dataops_sdk/schemas/hazard.py +151 -0
  38. dataops_sdk/schemas/meta.py +175 -0
  39. dataops_sdk/schemas/query.py +175 -0
  40. dataops_sdk/schemas/report.py +22 -0
  41. dataops_sdk/schemas/skill.py +70 -0
  42. dataops_sdk/schemas/table.py +65 -0
  43. dataops_sdk/schemas/user.py +52 -0
  44. dataops_sdk/schemas/widget.py +651 -0
  45. dataops_sdk/schemas/workflow.py +63 -0
  46. dataops_sdk/services/__init__.py +110 -0
  47. dataops_sdk/services/alarm.py +324 -0
  48. dataops_sdk/services/api.py +28 -0
  49. dataops_sdk/services/auth.py +120 -0
  50. dataops_sdk/services/data_table.py +485 -0
  51. dataops_sdk/services/dataset.py +127 -0
  52. dataops_sdk/services/datasource.py +57 -0
  53. dataops_sdk/services/dict_.py +69 -0
  54. dataops_sdk/services/hazard.py +260 -0
  55. dataops_sdk/services/meta.py +523 -0
  56. dataops_sdk/services/metrics.py +42 -0
  57. dataops_sdk/services/ref.py +66 -0
  58. dataops_sdk/services/report.py +36 -0
  59. dataops_sdk/services/skill.py +276 -0
  60. dataops_sdk/services/table.py +340 -0
  61. dataops_sdk/services/widget.py +143 -0
  62. dataops_sdk/services/workflow.py +102 -0
  63. dataops_sdk/session.py +292 -0
  64. dataops_sdk/tiptap/__init__.py +30 -0
  65. dataops_sdk/tiptap/builder.py +75 -0
  66. dataops_sdk/tiptap/constants.py +133 -0
  67. dataops_sdk/tiptap/markdown_parser.py +294 -0
  68. dataops_sdk/tiptap/models.py +43 -0
  69. dataops_sdk/tiptap/parse_utils.py +889 -0
  70. dataops_sdk/tiptap/render_utils.py +846 -0
  71. dataops_sdk/tiptap/renderers.py +51 -0
  72. dataops_sdk/tiptap/tool.py +629 -0
  73. dataops_sdk/utils/__init__.py +15 -0
  74. dataops_sdk/utils/common.py +211 -0
  75. dataops_sdk/utils/utils.py +120 -0
  76. dataops_sdk-0.3.0.dist-info/METADATA +25 -0
  77. dataops_sdk-0.3.0.dist-info/RECORD +79 -0
  78. dataops_sdk-0.3.0.dist-info/WHEEL +5 -0
  79. dataops_sdk-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,889 @@
1
+ """TipTap Markdown parsing utilities.
2
+
3
+ Provides functions to parse Markdown text into TipTap JSON format.
4
+ """
5
+
6
+ import re
7
+ from typing import Any
8
+
9
+ from dataops_sdk.tiptap import constants
10
+
11
+
12
+ def create_doc(content: list[dict] | None = None) -> dict:
13
+ """Create a doc node.
14
+
15
+ Args:
16
+ content: List of content nodes
17
+
18
+ Returns:
19
+ TipTap doc node
20
+ """
21
+ doc = {"type": constants.NodeTypes.DOC}
22
+ if content:
23
+ doc["content"] = content
24
+ return doc
25
+
26
+
27
+ def create_node(
28
+ node_type: str,
29
+ attrs: dict | None = None,
30
+ content: list[dict] | None = None,
31
+ ) -> dict:
32
+ """Create a generic node.
33
+
34
+ Args:
35
+ node_type: Type of the node
36
+ attrs: Node attributes
37
+ content: Child content nodes
38
+
39
+ Returns:
40
+ TipTap node
41
+ """
42
+ node: dict[str, Any] = {"type": node_type}
43
+ if attrs:
44
+ node["attrs"] = attrs
45
+ if content:
46
+ node["content"] = content
47
+ return node
48
+
49
+
50
+ def create_text_node(text: str, marks: list[dict] | None = None) -> dict:
51
+ """Create a text node.
52
+
53
+ Args:
54
+ text: Text content
55
+ marks: List of marks to apply
56
+
57
+ Returns:
58
+ TipTap text node
59
+ """
60
+ node: dict[str, Any] = {"type": constants.NodeTypes.TEXT, "text": text or ""}
61
+ if marks:
62
+ node["marks"] = marks
63
+ return node
64
+
65
+
66
+ def parse_paragraph(tokens: list[dict] | None = None) -> dict:
67
+ """Parse a paragraph node.
68
+
69
+ Args:
70
+ tokens: Child tokens
71
+
72
+ Returns:
73
+ TipTap paragraph node
74
+ """
75
+ node = create_node(constants.NodeTypes.PARAGRAPH)
76
+ if tokens:
77
+ node["content"] = tokens
78
+ return node
79
+
80
+
81
+ def parse_heading(tokens: list[dict], level: int) -> dict:
82
+ """Parse a heading node.
83
+
84
+ Args:
85
+ tokens: Child tokens
86
+ level: Heading level (1-6)
87
+
88
+ Returns:
89
+ TipTap heading node
90
+ """
91
+ return create_node(
92
+ constants.NodeTypes.HEADING,
93
+ attrs={constants.AttrTypes.LEVEL: level},
94
+ content=tokens,
95
+ )
96
+
97
+
98
+ def parse_text(text: str) -> dict:
99
+ """Parse a text node.
100
+
101
+ Args:
102
+ text: Text content
103
+
104
+ Returns:
105
+ TipTap text node
106
+ """
107
+ return create_text_node(text, None)
108
+
109
+
110
+ def parse_code_block(code: str, language: str | None = None) -> dict:
111
+ """Parse a code block node.
112
+
113
+ Args:
114
+ code: Code content
115
+ language: Code language
116
+
117
+ Returns:
118
+ TipTap code block node
119
+ """
120
+ node = create_node(constants.NodeTypes.CODE_BLOCK)
121
+ content = [create_text_node(code)]
122
+ node["content"] = content
123
+ if language:
124
+ node["attrs"] = {constants.AttrTypes.LANGUAGE: language}
125
+ return node
126
+
127
+
128
+ def parse_blockquote(tokens: list[dict] | None = None) -> dict:
129
+ """Parse a blockquote node.
130
+
131
+ Args:
132
+ tokens: Child tokens
133
+
134
+ Returns:
135
+ TipTap blockquote node
136
+ """
137
+ node = create_node(constants.NodeTypes.BLOCKQUOTE)
138
+ if tokens:
139
+ node["content"] = tokens
140
+ return node
141
+
142
+
143
+ def parse_bullet_list(items: list[dict] | None = None) -> dict:
144
+ """Parse a bullet list node.
145
+
146
+ Args:
147
+ items: List items
148
+
149
+ Returns:
150
+ TipTap bullet list node
151
+ """
152
+ node = create_node(constants.NodeTypes.BULLET_LIST)
153
+ if items:
154
+ node["content"] = items
155
+ return node
156
+
157
+
158
+ def parse_ordered_list(items: list[dict] | None = None) -> dict:
159
+ """Parse an ordered list node.
160
+
161
+ Args:
162
+ items: List items
163
+
164
+ Returns:
165
+ TipTap ordered list node
166
+ """
167
+ node = create_node(constants.NodeTypes.ORDERED_LIST)
168
+ if items:
169
+ node["content"] = items
170
+ return node
171
+
172
+
173
+ def parse_task_list(items: list[dict] | None = None) -> dict:
174
+ """Parse a task list node.
175
+
176
+ Args:
177
+ items: Task items
178
+
179
+ Returns:
180
+ TipTap task list node
181
+ """
182
+ node = create_node(constants.NodeTypes.TASK_LIST)
183
+ if items:
184
+ node["content"] = items
185
+ return node
186
+
187
+
188
+ def parse_task_item(text: str, checked: bool) -> dict:
189
+ """Parse a task item node.
190
+
191
+ Args:
192
+ text: Task item text
193
+ checked: Whether the task is checked
194
+
195
+ Returns:
196
+ TipTap task item node
197
+ """
198
+ node = create_node(
199
+ constants.NodeTypes.TASK_ITEM,
200
+ attrs={constants.AttrTypes.CHECKED: checked},
201
+ content=[create_text_node(text)],
202
+ )
203
+ return node
204
+
205
+
206
+ def parse_list_item(tokens: list[dict] | None = None) -> dict:
207
+ """Parse a list item node.
208
+
209
+ Args:
210
+ tokens: Child tokens
211
+
212
+ Returns:
213
+ TipTap list item node
214
+ """
215
+ node = create_node(constants.NodeTypes.LIST_ITEM)
216
+ if tokens:
217
+ node["content"] = tokens
218
+ return node
219
+
220
+
221
+ def parse_horizontal_rule() -> dict:
222
+ """Parse a horizontal rule node.
223
+
224
+ Returns:
225
+ TipTap horizontal rule node
226
+ """
227
+ return create_node(constants.NodeTypes.HORIZONTAL_RULE)
228
+
229
+
230
+ def parse_hard_break() -> dict:
231
+ """Parse a hard break node.
232
+
233
+ Returns:
234
+ TipTap hard break node
235
+ """
236
+ return create_node(constants.NodeTypes.HARD_BREAK)
237
+
238
+
239
+ def parse_image(src: str, alt: str = "", title: str | None = None) -> dict:
240
+ """Parse an image node.
241
+
242
+ Args:
243
+ src: Image source URL
244
+ alt: Alt text
245
+ title: Image title
246
+
247
+ Returns:
248
+ TipTap image node
249
+ """
250
+ return create_node(
251
+ constants.NodeTypes.IMAGE,
252
+ attrs={
253
+ constants.AttrTypes.SRC: src or "",
254
+ constants.AttrTypes.ALT: alt or "",
255
+ constants.AttrTypes.TITLE: title or "",
256
+ },
257
+ )
258
+
259
+
260
+ def parse_table(rows: list[dict] | None = None) -> dict:
261
+ """Parse a table node.
262
+
263
+ Args:
264
+ rows: Table rows
265
+
266
+ Returns:
267
+ TipTap table node
268
+ """
269
+ node = create_node(constants.NodeTypes.TABLE)
270
+ if rows:
271
+ node["content"] = rows
272
+ return node
273
+
274
+
275
+ def parse_table_row(cells: list[dict] | None = None) -> dict:
276
+ """Parse a table row node.
277
+
278
+ Args:
279
+ cells: Row cells
280
+
281
+ Returns:
282
+ TipTap table row node
283
+ """
284
+ node = create_node(constants.NodeTypes.TABLE_ROW)
285
+ if cells:
286
+ node["content"] = cells
287
+ return node
288
+
289
+
290
+ def parse_table_cell(tokens: list[dict] | None = None) -> dict:
291
+ """Parse a table cell node.
292
+
293
+ Args:
294
+ tokens: Cell content tokens
295
+
296
+ Returns:
297
+ TipTap table cell node
298
+ """
299
+ node = create_node(constants.NodeTypes.TABLE_CELL)
300
+ if tokens:
301
+ node["content"] = tokens
302
+ return node
303
+
304
+
305
+ def parse_table_header(tokens: list[dict] | None = None) -> dict:
306
+ """Parse a table header node.
307
+
308
+ Args:
309
+ tokens: Header content tokens
310
+
311
+ Returns:
312
+ TipTap table header node
313
+ """
314
+ node = create_node(constants.NodeTypes.TABLE_HEADER)
315
+ if tokens:
316
+ node["content"] = tokens
317
+ return node
318
+
319
+
320
+ def parse_callout(
321
+ callout_type: str,
322
+ title: str | None,
323
+ content: list[str],
324
+ collapsible: bool,
325
+ collapsed: bool,
326
+ ) -> dict:
327
+ """Parse a callout node.
328
+
329
+ Args:
330
+ callout_type: Callout type (info/warning/success/error)
331
+ title: Callout title
332
+ content: Content lines
333
+ collapsible: Whether callout is collapsible
334
+ collapsed: Whether callout is collapsed
335
+
336
+ Returns:
337
+ TipTap callout node
338
+ """
339
+ node = create_node(
340
+ constants.NodeTypes.CALLOUT,
341
+ attrs={
342
+ constants.AttrTypes.CALLOUT_TYPE: callout_type or "info",
343
+ constants.AttrTypes.CALLOUT_TITLE: title or "",
344
+ constants.AttrTypes.CALLOUT_COLLAPSIBLE: collapsible,
345
+ constants.AttrTypes.CALLOUT_COLLAPSED: collapsed,
346
+ },
347
+ )
348
+
349
+ if content:
350
+ content_nodes = []
351
+ current_para: list[str] = []
352
+
353
+ for line in content:
354
+ if not line.strip():
355
+ if current_para:
356
+ content_nodes.append(
357
+ parse_paragraph(parse_inline_formatting(" ".join(current_para).strip()))
358
+ )
359
+ current_para = []
360
+ else:
361
+ current_para.append(line)
362
+
363
+ if current_para:
364
+ content_nodes.append(
365
+ parse_paragraph(parse_inline_formatting(" ".join(current_para).strip()))
366
+ )
367
+
368
+ if content_nodes:
369
+ node["content"] = content_nodes
370
+
371
+ return node
372
+
373
+
374
+ def parse_columns(
375
+ count: int,
376
+ gap: str | None,
377
+ columns_content: list[list[dict]],
378
+ ) -> dict:
379
+ """Parse a columns node.
380
+
381
+ Args:
382
+ count: Number of columns
383
+ gap: Gap between columns
384
+ columns_content: Content for each column
385
+
386
+ Returns:
387
+ TipTap columns node
388
+ """
389
+ node = create_node(
390
+ constants.NodeTypes.COLUMNS,
391
+ attrs={
392
+ constants.AttrTypes.COLUMNS_COUNT: count,
393
+ constants.AttrTypes.COLUMNS_GAP: gap,
394
+ },
395
+ )
396
+
397
+ content = []
398
+ column_width = 100 / max(count, 1)
399
+
400
+ for i, col_content in enumerate(columns_content):
401
+ column_node = create_node(
402
+ constants.NodeTypes.COLUMN,
403
+ attrs={constants.AttrTypes.COLUMN_WIDTH: f"{column_width:.2f}%"},
404
+ )
405
+ if col_content:
406
+ column_node["content"] = col_content
407
+ content.append(column_node)
408
+
409
+ if i < len(columns_content) - 1:
410
+ content.append(create_node(constants.NodeTypes.COLUMN_RESIZER))
411
+
412
+ if content:
413
+ node["content"] = content
414
+
415
+ return node
416
+
417
+
418
+ def parse_markdown(markdown: str) -> dict:
419
+ """Parse Markdown text to TipTap JSON document.
420
+
421
+ Args:
422
+ markdown: Markdown text
423
+
424
+ Returns:
425
+ TipTap JSON document
426
+ """
427
+ if not markdown:
428
+ return create_doc([])
429
+
430
+ content = _parse_markdown_content(markdown.strip())
431
+ return create_doc(content)
432
+
433
+
434
+ def _parse_markdown_content(markdown: str) -> list[dict]:
435
+ """Parse Markdown content into node list.
436
+
437
+ Args:
438
+ markdown: Markdown text
439
+
440
+ Returns:
441
+ List of TipTap nodes
442
+ """
443
+ content: list[dict] = []
444
+ lines = markdown.split("\n")
445
+ i = 0
446
+
447
+ while i < len(lines):
448
+ line = lines[i]
449
+
450
+ if not line.strip():
451
+ i += 1
452
+ continue
453
+
454
+ # Parse headings
455
+ heading_match = re.match(r"^(#{1,6})\s+(.+)$", line)
456
+ if heading_match:
457
+ level = len(heading_match.group(1))
458
+ text = heading_match.group(2).strip()
459
+ content.append(parse_heading(_parse_inline_formatting(text), level))
460
+ i += 1
461
+ continue
462
+
463
+ # Parse horizontal rule
464
+ if re.match(r"^(\*\s*\*\s*\*|-{3,}|_{3,})$", line.strip()):
465
+ content.append(parse_horizontal_rule())
466
+ i += 1
467
+ continue
468
+
469
+ # Parse code block
470
+ code_match = re.match(r"^```(\w+)?\s*$", line)
471
+ if code_match:
472
+ language = code_match.group(1)
473
+ code_lines: list[str] = []
474
+ i += 1
475
+
476
+ while i < len(lines) and not re.match(r"^```\s*$", lines[i]):
477
+ code_lines.append(lines[i])
478
+ i += 1
479
+
480
+ code = "\n".join(code_lines)
481
+ content.append(parse_code_block(code, language))
482
+ i += 1
483
+ continue
484
+
485
+ # Parse blockquote
486
+ if line.strip().startswith(">"):
487
+ quote_lines: list[str] = []
488
+ quote_text = line.strip()[1:].strip()
489
+ if quote_text:
490
+ quote_lines.append(quote_text)
491
+ i += 1
492
+
493
+ while i < len(lines) and lines[i].strip().startswith(">"):
494
+ current_line = lines[i].strip()[1:].strip()
495
+ if current_line:
496
+ quote_lines.append(current_line)
497
+ i += 1
498
+
499
+ joined_text = "\n".join(quote_lines)
500
+ content.append(
501
+ parse_blockquote([parse_paragraph(_parse_inline_formatting(joined_text))])
502
+ )
503
+ continue
504
+
505
+ # Parse lists
506
+ list_match = re.match(r"^(\s*)([-+*]|\d+\.)\s+(.+)$", line)
507
+ if list_match:
508
+ indent = list_match.group(1)
509
+ list_type = list_match.group(2)
510
+ item_text = list_match.group(3)
511
+
512
+ # Detect task list
513
+ task_match = re.match(r"^(\[([ xX])\])\s+(.+)$", item_text)
514
+ is_task = bool(task_match)
515
+
516
+ list_items: list[dict] = []
517
+ current_line = i
518
+
519
+ while current_line < len(lines):
520
+ current = lines[current_line]
521
+ current_matcher = re.match(r"^(\s*)([-+*]|\d+\.)\s+(.+)$", current)
522
+
523
+ if not current_matcher:
524
+ break
525
+
526
+ this_indent = current_matcher.group(1)
527
+ this_list_type = current_matcher.group(2)
528
+ this_item_text = current_matcher.group(3)
529
+
530
+ # Check if same list
531
+ if len(this_indent) < len(indent):
532
+ break
533
+
534
+ if is_task:
535
+ current_task_match = re.match(r"^(\[([ xX])\])\s+(.+)$", this_item_text)
536
+ if current_task_match:
537
+ checked = current_task_match.group(2).lower() == "x"
538
+ task_text = current_task_match.group(3)
539
+ list_items.append(parse_task_item(task_text, checked))
540
+ else:
541
+ break
542
+ else:
543
+ current_task_match = re.match(r"^(\[([ xX])\])\s+(.+)$", this_item_text)
544
+ if not current_task_match:
545
+ list_items.append(
546
+ parse_list_item(
547
+ [parse_paragraph(_parse_inline_formatting(this_item_text))]
548
+ )
549
+ )
550
+ else:
551
+ break
552
+
553
+ current_line += 1
554
+
555
+ if is_task:
556
+ content.append(parse_task_list(list_items))
557
+ else:
558
+ if re.match(r"^[-+*]$", list_type):
559
+ content.append(parse_bullet_list(list_items))
560
+ else:
561
+ content.append(parse_ordered_list(list_items))
562
+
563
+ i = current_line
564
+ continue
565
+
566
+ # Parse table
567
+ table_match = re.match(r"^\|(.+)\|$", line)
568
+ if table_match:
569
+ table_rows: list[dict] = []
570
+ current_line = i
571
+
572
+ while current_line < len(lines):
573
+ current = lines[current_line]
574
+ row_match = re.match(r"^\|(.+)\|$", current)
575
+
576
+ if not row_match:
577
+ break
578
+
579
+ row_content = row_match.group(1)
580
+
581
+ # Skip separator row
582
+ if re.match(r"^[\s\-:|]+$", row_content):
583
+ current_line += 1
584
+ continue
585
+
586
+ # Parse cells
587
+ cells = row_content.split("|")
588
+ row_cells: list[dict] = []
589
+ is_header = len(table_rows) == 0
590
+
591
+ for cell_text in cells:
592
+ trimmed_cell = cell_text.strip()
593
+ cell_type = (
594
+ constants.NodeTypes.TABLE_HEADER
595
+ if is_header
596
+ else constants.NodeTypes.TABLE_CELL
597
+ )
598
+ row_cells.append(
599
+ create_node(cell_type, content=_parse_inline_formatting(trimmed_cell))
600
+ )
601
+
602
+ table_rows.append(parse_table_row(row_cells))
603
+ current_line += 1
604
+
605
+ if table_rows:
606
+ content.append(parse_table(table_rows))
607
+
608
+ i = current_line
609
+ continue
610
+
611
+ # Parse <anti:html cmp="..."> custom components
612
+ anti_html_match = re.match(r"^<anti:html\s+cmp=\"(\w+)\"(.*)>$", line)
613
+ if anti_html_match:
614
+ cmp_type = anti_html_match.group(1)
615
+ attrs = anti_html_match.group(2)
616
+
617
+ if cmp_type == "columns":
618
+ # Parse <anti:html type="columns" count="n" gap="m">
619
+ count = 2
620
+ gap = None
621
+
622
+ if attrs:
623
+ count_match = re.search(r"count=\"(\d+)\"", attrs)
624
+ if count_match:
625
+ count = int(count_match.group(1))
626
+ gap_match = re.search(r"gap=\"([^\"]*)\"", attrs)
627
+ if gap_match:
628
+ gap = gap_match.group(1)
629
+
630
+ # Parse each <column> content
631
+ column_lines: list[list[str]] = []
632
+ i += 1
633
+
634
+ while i < len(lines):
635
+ current_line_text = lines[i].strip()
636
+
637
+ if current_line_text == "</anti:html>":
638
+ break
639
+
640
+ column_match = re.match(r"^<column(?:\s+(.+))?>$", current_line_text)
641
+ if column_match:
642
+ col_content: list[str] = []
643
+ i += 1
644
+
645
+ while i < len(lines) and lines[i].strip() != "</column>":
646
+ col_content.append(lines[i])
647
+ i += 1
648
+
649
+ column_lines.append(col_content)
650
+ i += 1
651
+ continue
652
+
653
+ i += 1
654
+
655
+ # Parse each column content to nodes
656
+ columns_content: list[list[dict]] = []
657
+ for col_lines in column_lines:
658
+ col_markdown = "\n".join(col_lines)
659
+ if col_markdown.strip():
660
+ col_nodes = _parse_markdown_content(col_markdown)
661
+ columns_content.append(col_nodes)
662
+ else:
663
+ columns_content.append([])
664
+
665
+ content.append(parse_columns(count, gap, columns_content))
666
+ i += 1
667
+ continue
668
+
669
+ elif cmp_type == "callout":
670
+ # Callout <anti:html cmp="callout" type="xxx" title="xxx" collapsible="true" collapsed="true">
671
+ callout_type = "info"
672
+ title = None
673
+ collapsible = False
674
+ collapsed = False
675
+
676
+ if attrs:
677
+ type_match = re.search(r"type=\"([^\"]*)\"", attrs)
678
+ if type_match:
679
+ callout_type = type_match.group(1)
680
+ title_match = re.search(r"title=\"([^\"]*)\"", attrs)
681
+ if title_match:
682
+ title = title_match.group(1)
683
+ collapsible_match = re.search(r"collapsible=\"([^\"]*)\"", attrs)
684
+ if collapsible_match:
685
+ collapsible = collapsible_match.group(1).lower() == "true"
686
+ collapsed_match = re.search(r"collapsed=\"([^\"]*)\"", attrs)
687
+ if collapsed_match:
688
+ collapsed = collapsed_match.group(1).lower() == "true"
689
+
690
+ callout_content: list[str] = []
691
+ i += 1
692
+
693
+ while i < len(lines) and lines[i].strip() != "</anti:html>":
694
+ callout_content.append(lines[i])
695
+ i += 1
696
+
697
+ i += 1
698
+ content.append(
699
+ parse_callout(callout_type, title, callout_content, collapsible, collapsed)
700
+ )
701
+ continue
702
+
703
+ # Parse paragraph
704
+ para_lines: list[str] = [line]
705
+ i += 1
706
+
707
+ while (
708
+ i < len(lines)
709
+ and lines[i].strip()
710
+ and not re.match(r"^(#{1,6})\s+", lines[i])
711
+ and not re.match(r"^(\*\s*\*\s*\*|-{3,}|_{3,})$", lines[i].strip())
712
+ and not re.match(r"^```", lines[i])
713
+ and not re.match(r"^>\s+", lines[i])
714
+ and not re.match(r"^(\s*)([-+*]|\d+\.)\s+", lines[i])
715
+ and not re.match(r"^\|(.+)\|$", lines[i])
716
+ and not re.match(r"^<anti:html\s+", lines[i])
717
+ ):
718
+ para_lines.append(lines[i])
719
+ i += 1
720
+
721
+ para_text = " ".join(para_lines)
722
+ content.append(parse_paragraph(_parse_inline_formatting(para_text)))
723
+
724
+ return content
725
+
726
+
727
+ def _parse_inline_formatting(text: str) -> list[dict]:
728
+ """Parse inline formatting (bold, italic, etc.).
729
+
730
+ Args:
731
+ text: Raw text
732
+
733
+ Returns:
734
+ List of text nodes with marks
735
+ """
736
+ result: list[dict] = []
737
+ pos = 0
738
+
739
+ # Priority order: code > link > highlight > bold > italic > strike > underline
740
+ patterns = [
741
+ (r"`([^`]+)`", "code"),
742
+ (r"\[([^\]]+)\]\(([^)]+)\)", "link"),
743
+ (r"==([^=]+)==", "highlight"),
744
+ (r"\*\*([^*]+)\*\*", "bold"),
745
+ (r"(?<!\*)\*(?!\*)([^*]+)(?<!\*)\*(?!\*)", "italic"),
746
+ (r"~~([^~]+)~~", "strike"),
747
+ (r"__([^_]+)__", "underline"),
748
+ ]
749
+
750
+ while pos < len(text):
751
+ best_match = None
752
+ best_pattern_index = -1
753
+ best_start = len(text)
754
+
755
+ for j, (pattern, mark_type) in enumerate(patterns):
756
+ matcher = re.match(pattern, text[pos:])
757
+ if matcher:
758
+ if matcher.start() < best_start:
759
+ best_match = matcher
760
+ best_pattern_index = j
761
+ best_start = matcher.start()
762
+
763
+ if best_match is None:
764
+ result.append(create_text_node(text[pos:], None))
765
+ break
766
+
767
+ if best_match.start() > 0:
768
+ result.append(create_text_node(text[pos : pos + best_match.start()], None))
769
+
770
+ matched_text = best_match.group(1)
771
+ start_pos = pos + best_match.start()
772
+ mark_type = patterns[best_pattern_index][1]
773
+
774
+ if mark_type == "link":
775
+ link_text = best_match.group(1)
776
+ link_url = best_match.group(2)
777
+ link_node = create_text_node(link_text, None)
778
+ link_node["marks"] = [
779
+ {
780
+ "type": constants.MarkTypes.LINK,
781
+ "attrs": {
782
+ constants.AttrTypes.HREF: link_url,
783
+ constants.AttrTypes.TITLE: "",
784
+ },
785
+ }
786
+ ]
787
+ result.append(link_node)
788
+ elif mark_type == "code":
789
+ code_node = create_text_node(matched_text, None)
790
+ code_node["marks"] = [{"type": constants.MarkTypes.CODE}]
791
+ result.append(code_node)
792
+ else:
793
+ marked_node = create_text_node(matched_text, None)
794
+ marked_node["marks"] = [{"type": mark_type}]
795
+ result.append(marked_node)
796
+
797
+ pos = start_pos + best_match.end()
798
+
799
+ if not result:
800
+ result.append(create_text_node(text, None))
801
+
802
+ # Merge adjacent plain text nodes
803
+ merged_result: list[dict] = []
804
+ for node in result:
805
+ node_text = node.get("text")
806
+ node_marks = node.get("marks")
807
+
808
+ if node_text and (node_marks is None or len(node_marks) == 0):
809
+ if merged_result:
810
+ last_node = merged_result[-1]
811
+ last_text = last_node.get("text")
812
+ last_marks = last_node.get("marks")
813
+
814
+ if last_text and (last_marks is None or len(last_marks) == 0):
815
+ last_node["text"] = last_text + node_text
816
+ continue
817
+
818
+ merged_result.append(node)
819
+
820
+ return merged_result
821
+
822
+
823
+ def parse_fallback(token_type: str, token: dict) -> dict | None:
824
+ """Fallback parser for handling unhandled types.
825
+
826
+ Args:
827
+ token_type: Token type
828
+ token: Token data
829
+
830
+ Returns:
831
+ Parsed node or None
832
+ """
833
+ if token_type == "paragraph":
834
+ return parse_paragraph(token.get("tokens"))
835
+ elif token_type == "heading":
836
+ return parse_heading(token.get("tokens"), token.get("depth", 1))
837
+ elif token_type == "text":
838
+ return parse_text(token.get("text", ""))
839
+ return None
840
+
841
+
842
+ def pick_tags(content: str) -> dict[str, list[dict]]:
843
+ """Extract all HTML tags from content.
844
+
845
+ Args:
846
+ content: HTML content
847
+
848
+ Returns:
849
+ Dict mapping tag names to list of tag objects
850
+ """
851
+ result: dict[str, list[dict]] = {}
852
+
853
+ if not content or not content.strip():
854
+ return result
855
+
856
+ tag_pattern = r"<(\w+)([^>]*)>(.*?)</\1>|<(\w+)([^>]*)/>"
857
+ pattern = re.compile(tag_pattern, re.DOTALL)
858
+ matcher = pattern.finditer(content)
859
+
860
+ for match in matcher:
861
+ if match.group(1) is not None:
862
+ tag_name = match.group(1)
863
+ tag_attrs = match.group(2)
864
+ tag_content = match.group(3)
865
+ else:
866
+ tag_name = match.group(4)
867
+ tag_attrs = match.group(5)
868
+ tag_content = None
869
+
870
+ tag_obj: dict[str, Any] = {}
871
+
872
+ attr_pattern = re.compile(r'(\w+)="([^"]*)"|(\w+)=' "'([^']*)'")
873
+ attr_matcher = attr_pattern.finditer(tag_attrs or "")
874
+
875
+ for attr_match in attr_matcher:
876
+ attr_name = attr_match.group(1) or attr_match.group(3)
877
+ attr_value = attr_match.group(2) or attr_match.group(4)
878
+ if attr_name:
879
+ tag_obj[attr_name] = attr_value
880
+
881
+ if tag_content and tag_content.strip():
882
+ tag_obj["content"] = tag_content.strip()
883
+
884
+ if tag_name:
885
+ if tag_name not in result:
886
+ result[tag_name] = []
887
+ result[tag_name].append(tag_obj)
888
+
889
+ return result