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,846 @@
1
+ """TipTap rendering utilities.
2
+
3
+ Provides functions to render TipTap JSON to Markdown format.
4
+ """
5
+
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from dataops_sdk.tiptap import constants
9
+
10
+ if TYPE_CHECKING:
11
+ from dataops_sdk.tiptap.tool import TipTapTool
12
+
13
+
14
+ def render_paragraph(node: dict, content: str, parent_type: str | None) -> str:
15
+ """Render a paragraph node.
16
+
17
+ Args:
18
+ node: Paragraph node
19
+ content: Rendered child content
20
+ parent_type: Parent node type
21
+
22
+ Returns:
23
+ Markdown formatted paragraph
24
+ """
25
+ if content is None:
26
+ content = ""
27
+ return content + constants.Markdown.PARAGRAPH_SEPARATOR
28
+
29
+
30
+ def render_heading(node: dict, level: int, content: str) -> str:
31
+ """Render a heading node.
32
+
33
+ Args:
34
+ node: Heading node
35
+ level: Heading level
36
+ content: Rendered child content
37
+
38
+ Returns:
39
+ Markdown formatted heading
40
+ """
41
+ if content is None:
42
+ content = ""
43
+ return f"{constants.Markdown.HEADING_PREFIX * level} {content}{constants.Markdown.PARAGRAPH_SEPARATOR}"
44
+
45
+
46
+ def render_code_block(node: dict, content: str) -> str:
47
+ """Render a code block node.
48
+
49
+ Args:
50
+ node: Code block node
51
+ content: Rendered child content
52
+
53
+ Returns:
54
+ Markdown formatted code block
55
+ """
56
+ language = ""
57
+ attrs = node.get("attrs")
58
+ if attrs:
59
+ language = attrs.get(constants.AttrTypes.LANGUAGE, "")
60
+ if content is None:
61
+ content = ""
62
+ return (
63
+ f"{constants.Markdown.CODE_BLOCK_OPEN}{language}\n"
64
+ f"{content}\n"
65
+ f"{constants.Markdown.CODE_BLOCK_CLOSE}"
66
+ f"{constants.Markdown.PARAGRAPH_SEPARATOR}"
67
+ )
68
+
69
+
70
+ def render_blockquote(content: str) -> str:
71
+ """Render a blockquote node.
72
+
73
+ Args:
74
+ content: Rendered child content
75
+
76
+ Returns:
77
+ Markdown formatted blockquote
78
+ """
79
+ if not content or not content.strip():
80
+ return ""
81
+
82
+ lines = content.strip().split("\n")
83
+ result = []
84
+ for line in lines:
85
+ if not line:
86
+ result.append(f"{constants.Markdown.BLOCKQUOTE_PREFIX}\n")
87
+ else:
88
+ result.append(f"{constants.Markdown.BLOCKQUOTE_PREFIX}{line}\n")
89
+ return "".join(result) + constants.Markdown.PARAGRAPH_SEPARATOR
90
+
91
+
92
+ def render_bullet_list(content: str) -> str:
93
+ """Render a bullet list node.
94
+
95
+ Args:
96
+ content: Rendered child content
97
+
98
+ Returns:
99
+ Markdown formatted bullet list
100
+ """
101
+ if not content:
102
+ return ""
103
+ return content
104
+
105
+
106
+ def render_ordered_list(content: str) -> str:
107
+ """Render an ordered list node.
108
+
109
+ Args:
110
+ content: Rendered child content
111
+
112
+ Returns:
113
+ Markdown formatted ordered list
114
+ """
115
+ if not content:
116
+ return ""
117
+ return content
118
+
119
+
120
+ def render_list_item(
121
+ node: dict,
122
+ content: str,
123
+ parent_type: str | None,
124
+ index: int,
125
+ ) -> str:
126
+ """Render a list item node.
127
+
128
+ Args:
129
+ node: List item node
130
+ content: Rendered child content
131
+ parent_type: Parent node type
132
+ index: Index of this item
133
+
134
+ Returns:
135
+ Markdown formatted list item
136
+ """
137
+ if content is None:
138
+ return ""
139
+
140
+ if parent_type == constants.NodeTypes.ORDERED_LIST:
141
+ prefix = f"{index + 1}. "
142
+ else:
143
+ prefix = constants.Markdown.BULLET_LIST_PREFIX
144
+
145
+ lines = content.split("\n")
146
+ if len(lines) == 1:
147
+ return f"{prefix}{lines[0]}\n"
148
+
149
+ result = [f"{prefix}{lines[0]}\n"]
150
+ indent = " " * len(prefix)
151
+ for line in lines[1:]:
152
+ result.append(f"{indent}{line}\n")
153
+ return "".join(result)
154
+
155
+
156
+ def render_horizontal_rule() -> str:
157
+ """Render a horizontal rule node.
158
+
159
+ Returns:
160
+ Markdown formatted horizontal rule
161
+ """
162
+ return constants.Markdown.HORIZONTAL_RULE + constants.Markdown.PARAGRAPH_SEPARATOR
163
+
164
+
165
+ def render_hard_break() -> str:
166
+ """Render a hard break node.
167
+
168
+ Returns:
169
+ Markdown formatted hard break
170
+ """
171
+ return constants.Markdown.HARD_BREAK_SUFFIX
172
+
173
+
174
+ def render_image(node: dict) -> str:
175
+ """Render an image node.
176
+
177
+ Args:
178
+ node: Image node
179
+
180
+ Returns:
181
+ Markdown formatted image
182
+ """
183
+ attrs = node.get("attrs")
184
+ src = attrs.get(constants.AttrTypes.SRC, "") if attrs else ""
185
+ alt = attrs.get(constants.AttrTypes.ALT, "") if attrs else ""
186
+ title = attrs.get(constants.AttrTypes.TITLE, "") if attrs else ""
187
+
188
+ if alt:
189
+ if title:
190
+ image_markdown = f'![{alt}]({src} "{title}")'
191
+ else:
192
+ image_markdown = f"![{alt}]({src})"
193
+ else:
194
+ if title:
195
+ image_markdown = f'![]({src} "{title}")'
196
+ else:
197
+ image_markdown = f"![]({src})"
198
+
199
+ return image_markdown + constants.Markdown.PARAGRAPH_SEPARATOR
200
+
201
+
202
+ def render_table_to_md_format(node: dict) -> str:
203
+ """Render a table node to Markdown format.
204
+
205
+ Args:
206
+ node: Table node
207
+
208
+ Returns:
209
+ Markdown formatted table
210
+ """
211
+ content = node.get("content")
212
+ if not content:
213
+ return ""
214
+
215
+ rows_data: list[list[tuple[str, bool]]] = []
216
+
217
+ for row_node in content:
218
+ cells = row_node.get("content")
219
+ if not cells:
220
+ continue
221
+
222
+ row: list[tuple[str, bool]] = []
223
+ for cell_node in cells:
224
+ cell_text = _extract_cell_text(cell_node)
225
+ is_header = cell_node.get("type") == constants.NodeTypes.TABLE_HEADER
226
+ row.append((cell_text, is_header))
227
+ rows_data.append(row)
228
+
229
+ if not rows_data:
230
+ return ""
231
+
232
+ column_count = max(len(row) for row in rows_data)
233
+ if column_count == 0:
234
+ return ""
235
+
236
+ col_widths = [3] * column_count
237
+ for row in rows_data:
238
+ for i, (cell_text, _) in enumerate(row):
239
+ if len(cell_text) > col_widths[i]:
240
+ col_widths[i] = len(cell_text)
241
+
242
+ header_row = rows_data[0]
243
+ has_header = any(is_header for _, is_header in header_row)
244
+
245
+ out_lines: list[str] = []
246
+
247
+ header_texts = []
248
+ for i in range(column_count):
249
+ if has_header and i < len(header_row):
250
+ header_texts.append(header_row[i][0])
251
+ else:
252
+ header_texts.append("")
253
+
254
+ out_lines.append(
255
+ "| " + " | ".join(_pad_text(t, w) for t, w in zip(header_texts, col_widths)) + " |"
256
+ )
257
+ out_lines.append("| " + " | ".join("-" * max(3, w) for w in col_widths) + " |")
258
+
259
+ start_row = 1 if has_header else 0
260
+ for r in range(start_row, len(rows_data)):
261
+ row = rows_data[r]
262
+ cell_texts = []
263
+ for i in range(column_count):
264
+ if i < len(row):
265
+ cell_texts.append(row[i][0])
266
+ else:
267
+ cell_texts.append("")
268
+ out_lines.append(
269
+ "| " + " | ".join(_pad_text(t, w) for t, w in zip(cell_texts, col_widths)) + " |"
270
+ )
271
+
272
+ return "".join(line + "\n" for line in out_lines) + "\n"
273
+
274
+
275
+ def render_table_to_html_format(tool: "TipTapTool", node: dict) -> str:
276
+ """Render a table node to HTML format.
277
+
278
+ Args:
279
+ tool: TipTapTool instance
280
+ node: Table node
281
+
282
+ Returns:
283
+ HTML formatted table
284
+ """
285
+ content = node.get("content")
286
+ if not content:
287
+ return ""
288
+
289
+ rows_data: list[list[dict[str, Any]]] = []
290
+
291
+ for row_node in content:
292
+ cells = row_node.get("content")
293
+ if not cells:
294
+ continue
295
+
296
+ row: list[dict[str, Any]] = []
297
+ for cell_node in cells:
298
+ cell_text = _extract_cell_content(tool, cell_node)
299
+ is_header = cell_node.get("type") == constants.NodeTypes.TABLE_HEADER
300
+
301
+ attrs = cell_node.get("attrs") or {}
302
+ colspan = attrs.get(constants.AttrTypes.COLSPAN, 1)
303
+ rowspan = attrs.get(constants.AttrTypes.ROWSPAN, 1)
304
+ alignment = attrs.get(constants.AttrTypes.ALIGNMENT, "")
305
+
306
+ html_attrs = ""
307
+ if colspan > 1:
308
+ html_attrs += f' colspan="{colspan}"'
309
+ if rowspan > 1:
310
+ html_attrs += f' rowspan="{rowspan}"'
311
+ if alignment:
312
+ html_attrs += f' style="text-align: {alignment};"'
313
+
314
+ row.append({"text": cell_text, "is_header": is_header, "attrs": html_attrs})
315
+ rows_data.append(row)
316
+
317
+ if not rows_data:
318
+ return ""
319
+
320
+ out = "<table>\n"
321
+
322
+ has_header_row = rows_data[0] and any(cell["is_header"] for cell in rows_data[0])
323
+
324
+ if has_header_row:
325
+ out += "<thead><tr>"
326
+ for cell in rows_data[0]:
327
+ tag = "th" if cell["is_header"] else "td"
328
+ attrs = f" {cell['attrs']}" if cell["attrs"] else ""
329
+ out += f"<{tag}{attrs}>{cell['text']}</{tag}>"
330
+ out += "</tr></thead>\n"
331
+ body_rows = rows_data[1:]
332
+ else:
333
+ body_rows = rows_data
334
+
335
+ out += "<tbody>\n"
336
+ for row in body_rows:
337
+ out += "<tr>"
338
+ for cell in row:
339
+ tag = "th" if cell["is_header"] else "td"
340
+ attrs = f" {cell['attrs']}" if cell["attrs"] else ""
341
+ out += f"<{tag}{attrs}>{cell['text']}</{tag}>"
342
+ out += "</tr>\n"
343
+ out += "</tbody></table>\n\n"
344
+
345
+ return out
346
+
347
+
348
+ def render_table_row() -> str:
349
+ """Render a table row node (placeholder).
350
+
351
+ Returns:
352
+ Empty string
353
+ """
354
+ return ""
355
+
356
+
357
+ def render_table_cell() -> str:
358
+ """Render a table cell node (placeholder).
359
+
360
+ Returns:
361
+ Empty string
362
+ """
363
+ return ""
364
+
365
+
366
+ def render_table_header() -> str:
367
+ """Render a table header node (placeholder).
368
+
369
+ Returns:
370
+ Empty string
371
+ """
372
+ return ""
373
+
374
+
375
+ def render_bold(content: str) -> str:
376
+ """Render bold mark.
377
+
378
+ Args:
379
+ content: Content to wrap
380
+
381
+ Returns:
382
+ Markdown formatted bold text
383
+ """
384
+ if content is None:
385
+ content = ""
386
+ return f"{constants.Markdown.BOLD_MARKER}{content}{constants.Markdown.BOLD_MARKER}"
387
+
388
+
389
+ def render_italic(content: str) -> str:
390
+ """Render italic mark.
391
+
392
+ Args:
393
+ content: Content to wrap
394
+
395
+ Returns:
396
+ Markdown formatted italic text
397
+ """
398
+ if content is None:
399
+ content = ""
400
+ return f"{constants.Markdown.ITALIC_MARKER}{content}{constants.Markdown.ITALIC_MARKER}"
401
+
402
+
403
+ def render_strike(content: str) -> str:
404
+ """Render strike mark.
405
+
406
+ Args:
407
+ content: Content to wrap
408
+
409
+ Returns:
410
+ Markdown formatted strike text
411
+ """
412
+ if content is None:
413
+ content = ""
414
+ return f"{constants.Markdown.STRIKE_MARKER}{content}{constants.Markdown.STRIKE_MARKER}"
415
+
416
+
417
+ def render_code(content: str) -> str:
418
+ """Render inline code mark.
419
+
420
+ Args:
421
+ content: Content to wrap
422
+
423
+ Returns:
424
+ Markdown formatted code text
425
+ """
426
+ if content is None:
427
+ content = ""
428
+ return f"{constants.Markdown.CODE_MARKER}{content}{constants.Markdown.CODE_MARKER}"
429
+
430
+
431
+ def render_link(node: dict, content: str) -> str:
432
+ """Render link mark.
433
+
434
+ Args:
435
+ node: Link node
436
+ content: Link text content
437
+
438
+ Returns:
439
+ Markdown formatted link
440
+ """
441
+ attrs = node.get("attrs")
442
+ href = attrs.get(constants.AttrTypes.HREF, "") if attrs else ""
443
+ title = attrs.get(constants.AttrTypes.TITLE, "") if attrs else ""
444
+
445
+ if title:
446
+ return f'[{content}]({href} "{title}")'
447
+ return f"[{content}]({href})"
448
+
449
+
450
+ def render_underline(content: str) -> str:
451
+ """Render underline mark.
452
+
453
+ Args:
454
+ content: Content to wrap
455
+
456
+ Returns:
457
+ HTML formatted underline text
458
+ """
459
+ if content is None:
460
+ content = ""
461
+ return f"{constants.Markdown.UNDERLINE_HTML_OPEN}{content}{constants.Markdown.UNDERLINE_HTML_CLOSE}"
462
+
463
+
464
+ def render_highlight(content: str) -> str:
465
+ """Render highlight mark.
466
+
467
+ Args:
468
+ content: Content to wrap
469
+
470
+ Returns:
471
+ Markdown formatted highlight text
472
+ """
473
+ if content is None:
474
+ content = ""
475
+ return f"{constants.Markdown.HIGHLIGHT_MARKER}{content}{constants.Markdown.HIGHLIGHT_MARKER}"
476
+
477
+
478
+ def wrap_in_block(prefix: str, content: str) -> str:
479
+ """Wrap content in a block with prefix.
480
+
481
+ Args:
482
+ prefix: Block prefix
483
+ content: Content to wrap
484
+
485
+ Returns:
486
+ Wrapped content
487
+ """
488
+ if not content:
489
+ return ""
490
+
491
+ lines = content.split("\n")
492
+ result = []
493
+ prefix_len = len(prefix) - 1 if prefix.endswith(" ") else len(prefix)
494
+
495
+ for i, line in enumerate(lines):
496
+ if not line:
497
+ result.append(prefix[:prefix_len] + "\n")
498
+ else:
499
+ result.append(prefix + line)
500
+ if i < len(lines) - 1:
501
+ result.append("\n")
502
+
503
+ return "".join(result)
504
+
505
+
506
+ def attrs_to_html_attrs(attrs: dict) -> str:
507
+ """Convert JSON attrs to HTML attribute string.
508
+
509
+ Args:
510
+ attrs: JSON attributes
511
+
512
+ Returns:
513
+ HTML attribute string like: id="123" name="test"
514
+ """
515
+ if not attrs:
516
+ return ""
517
+
518
+ result = []
519
+ for key, value in attrs.items():
520
+ if value is not None and isinstance(value, str) and value.strip():
521
+ result.append(f'{key}="{escape_html(value)}"')
522
+
523
+ return " " + " ".join(result) if result else ""
524
+
525
+
526
+ def escape_html(text: str) -> str:
527
+ """Escape HTML special characters.
528
+
529
+ Args:
530
+ text: Raw text
531
+
532
+ Returns:
533
+ Escaped text
534
+ """
535
+ if text is None:
536
+ return ""
537
+ return (
538
+ text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
539
+ )
540
+
541
+
542
+ def escape_markdown_special_chars(text: str) -> str:
543
+ """Escape Markdown special characters.
544
+
545
+ Args:
546
+ text: Raw text
547
+
548
+ Returns:
549
+ Escaped text
550
+ """
551
+ if text is None:
552
+ return ""
553
+ return text.replace("\\", "\\\\").replace("`", "\\`").replace("[", "\\[").replace("]", "\\]")
554
+
555
+
556
+ def _pad_text(text: str, width: int) -> str:
557
+ """Pad text to specified width.
558
+
559
+ Args:
560
+ text: Text to pad
561
+ width: Target width
562
+
563
+ Returns:
564
+ Padded text
565
+ """
566
+ if text is None:
567
+ text = ""
568
+ if len(text) >= width:
569
+ return text
570
+ return text + " " * (width - len(text))
571
+
572
+
573
+ def _extract_cell_text(cell_node: dict) -> str:
574
+ """Extract text from a cell node.
575
+
576
+ Args:
577
+ cell_node: Cell node
578
+
579
+ Returns:
580
+ Cell text
581
+ """
582
+ content = cell_node.get("content")
583
+ if not content:
584
+ return ""
585
+
586
+ parts = []
587
+ for child in content:
588
+ if not child:
589
+ continue
590
+
591
+ if child.get("type") == constants.NodeTypes.TEXT:
592
+ text = child.get("text")
593
+ if text:
594
+ parts.append(text)
595
+ elif "content" in child:
596
+ nested_text = _extract_cell_text(child)
597
+ if nested_text:
598
+ parts.append(nested_text)
599
+
600
+ return " ".join(parts).replace(r"\s+", " ").strip()
601
+
602
+
603
+ def _extract_cell_content(tool: "TipTapTool", cell_node: dict) -> str:
604
+ """Extract cell content with support for custom components.
605
+
606
+ Args:
607
+ tool: TipTapTool instance
608
+ cell_node: Cell node
609
+
610
+ Returns:
611
+ Cell content (HTML or plain text)
612
+ """
613
+ content = cell_node.get("content")
614
+ if not content:
615
+ return ""
616
+
617
+ for child in content:
618
+ if not child:
619
+ continue
620
+ return tool.serialize(child)
621
+
622
+ return escape_html(_extract_cell_text(cell_node))
623
+
624
+
625
+ def render_fixed_title(content: str) -> str:
626
+ """Render fixed title.
627
+
628
+ Args:
629
+ content: Content (unused)
630
+
631
+ Returns:
632
+ Double newline
633
+ """
634
+ return "\n\n"
635
+
636
+
637
+ def render_widget_num(node: dict) -> str:
638
+ """Render widget number component.
639
+
640
+ Args:
641
+ node: Widget node
642
+
643
+ Returns:
644
+ HTML formatted widget
645
+ """
646
+ attrs = node.get("attrs") or {}
647
+ node_id = attrs.get("id", "")
648
+ content = node.get("content")
649
+
650
+ sb = []
651
+ if content:
652
+ for item in content:
653
+ if not item:
654
+ continue
655
+
656
+ text_json = item.get("text")
657
+ if not text_json:
658
+ continue
659
+
660
+ try:
661
+ import json
662
+
663
+ params = json.loads(text_json)
664
+ code = params.get("code", "")
665
+ col_name = params.get("colName", "")
666
+ title = params.get("title", "")
667
+
668
+ sb.append(
669
+ f'<item type="widget" code="{escape_html(code)}" '
670
+ f'colName="{escape_html(col_name)}" title="{escape_html(title)}"></item>'
671
+ )
672
+ except (json.JSONDecodeError, TypeError):
673
+ continue
674
+
675
+ return f'<anti:html cmp="{constants.NodeTypes.WIDGET_NUM}" id="{escape_html(node_id)}">{"".join(sb)}</anti:html>'
676
+
677
+
678
+ def render_dataset_data(node: dict) -> str:
679
+ """Render dataset data component.
680
+
681
+ Args:
682
+ node: Dataset data node
683
+
684
+ Returns:
685
+ HTML formatted dataset component
686
+ """
687
+ attrs = node.get("attrs") or {}
688
+ node_id = attrs.get("id", "")
689
+ content = node.get("content")
690
+
691
+ sb = []
692
+ if content:
693
+ for item in content:
694
+ if not item:
695
+ continue
696
+
697
+ text_json = item.get("text")
698
+ if not text_json:
699
+ continue
700
+
701
+ try:
702
+ import json
703
+
704
+ params = json.loads(text_json)
705
+ code = params.get("code", "")
706
+ col_name = params.get("colName", "")
707
+ item_type = params.get("type", "dataset")
708
+
709
+ sb.append(
710
+ f'<item type="{escape_html(item_type)}" code="{escape_html(code)}" '
711
+ f'colName="{escape_html(col_name)}"></item>'
712
+ )
713
+ except (json.JSONDecodeError, TypeError):
714
+ continue
715
+
716
+ return f'<anti:html cmp="{constants.NodeTypes.DATASET_DATA}" id="{escape_html(node_id)}">{"".join(sb)}</anti:html>'
717
+
718
+
719
+ def render_callout(node: dict, content: str) -> str:
720
+ """Render callout component.
721
+
722
+ Args:
723
+ node: Callout node
724
+ content: Rendered child content
725
+
726
+ Returns:
727
+ Markdown formatted callout
728
+ """
729
+ attrs = node.get("attrs") or {}
730
+ ctype = attrs.get(constants.AttrTypes.CALLOUT_TYPE, "info")
731
+ title = attrs.get(constants.AttrTypes.CALLOUT_TITLE, "") or ""
732
+ collapsible = attrs.get(constants.AttrTypes.CALLOUT_COLLAPSIBLE, False)
733
+ collapsed = attrs.get(constants.AttrTypes.CALLOUT_COLLAPSED, False)
734
+
735
+ attrs_str = f'type="{ctype}"'
736
+ if title:
737
+ attrs_str += f' title="{escape_markdown_special_chars(title)}"'
738
+ if collapsible:
739
+ attrs_str += ' collapsible="true"'
740
+ if collapsed:
741
+ attrs_str += ' collapsed="true"'
742
+
743
+ return f'<anti:html cmp="{constants.NodeTypes.CALLOUT}" {attrs_str}>\n{content}\n</anti:html>\n'
744
+
745
+
746
+ def render_mention(node: dict) -> str:
747
+ """Render mention node.
748
+
749
+ Args:
750
+ node: Mention node
751
+
752
+ Returns:
753
+ HTML formatted mention
754
+ """
755
+ attrs = node.get("attrs") or {}
756
+ mention_type = attrs.get("type", "")
757
+ node_id = node.get("id", "")
758
+ code = attrs.get("id", "")
759
+ sub_type = attrs.get("subType", "")
760
+
761
+ import uuid
762
+
763
+ if not node_id:
764
+ node_id = str(uuid.uuid4())
765
+
766
+ if mention_type == "ontology":
767
+ return (
768
+ f'<mention id="{escape_html(node_id)}" type="report" '
769
+ f'sub-type="ontology" code="{escape_html(code)}"></mention>'
770
+ )
771
+
772
+ data = attrs.get("data")
773
+
774
+ mention_tag = f'<mention id="{escape_html(node_id)}" type="{escape_html(mention_type)}" code="{escape_html(code)}"'
775
+
776
+ if sub_type:
777
+ mention_tag += f' subType="{escape_html(sub_type)}"'
778
+ elif mention_type == "report":
779
+ mention_tag += ' subType="doc"'
780
+
781
+ if data:
782
+ import json
783
+
784
+ data_str = json.dumps(data) if isinstance(data, dict) else str(data)
785
+ mention_tag += f' data="{escape_html(data_str)}"'
786
+
787
+ mention_tag += "></mention>"
788
+ return mention_tag
789
+
790
+
791
+ def render_single_item_custom(node: dict, html_tag: str, inline: bool = False) -> str:
792
+ """Render single item custom node.
793
+
794
+ Args:
795
+ node: Node
796
+ html_tag: HTML tag to use
797
+ inline: Whether to render inline
798
+
799
+ Returns:
800
+ HTML formatted node
801
+ """
802
+ attrs = node.get("attrs") or {}
803
+
804
+ html_attrs = attrs_to_html_attrs(attrs)
805
+
806
+ text = ""
807
+ content = node.get("content")
808
+ if content:
809
+ first_item = content[0] if content else {}
810
+ text = first_item.get("text", "") or ""
811
+
812
+ result = f"<{html_tag}{html_attrs}>{text}</{html_tag}>"
813
+
814
+ if not inline:
815
+ result += "\n"
816
+
817
+ return result
818
+
819
+
820
+ def render_mermaid(node: dict) -> str:
821
+ """Render mermaid chart node.
822
+
823
+ Args:
824
+ node: Mermaid node
825
+
826
+ Returns:
827
+ Markdown formatted mermaid code block
828
+ """
829
+ attrs = node.get("attrs") or {}
830
+ chart_type = attrs.get(constants.AttrTypes.CHART_TYPE, "") or ""
831
+ title = attrs.get(constants.AttrTypes.TITLE, "") or ""
832
+ content = attrs.get(constants.AttrTypes.CODE, "") or ""
833
+
834
+ mermaid_content = "```mermaid\n"
835
+ if chart_type:
836
+ mermaid_content += chart_type
837
+ if title:
838
+ mermaid_content += f' title "{title}"'
839
+ if content:
840
+ if mermaid_content.endswith("\n"):
841
+ mermaid_content += "\n"
842
+ else:
843
+ mermaid_content += "\n"
844
+ mermaid_content += content
845
+
846
+ return mermaid_content + "\n```\n"