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.
- dataops_sdk/__init__.py +84 -0
- dataops_sdk/constants.py +6 -0
- dataops_sdk/exceptions.py +31 -0
- dataops_sdk/runner/__init__.py +46 -0
- dataops_sdk/runner/datasource_factory.py +83 -0
- dataops_sdk/runner/schemas/nacos/__init__.py +5 -0
- dataops_sdk/runner/schemas/nacos/config.py +40 -0
- dataops_sdk/runner/schemas/nacos/constants.py +235 -0
- dataops_sdk/runner/schemas/nacos/instance.py +103 -0
- dataops_sdk/runner/schemas/nacos/nacos_exception.py +44 -0
- dataops_sdk/runner/schemas/nacos/naming_param.py +62 -0
- dataops_sdk/runner/schemas/nacos/naming_request.py +84 -0
- dataops_sdk/runner/schemas/nacos/naming_response.py +50 -0
- dataops_sdk/runner/schemas/nacos/preserved_metadata_key.py +10 -0
- dataops_sdk/runner/schemas/nacos/service.py +119 -0
- dataops_sdk/runner/utils/__init__.py +42 -0
- dataops_sdk/runner/utils/common.py +209 -0
- dataops_sdk/runner/utils/dbConn_base.py +256 -0
- dataops_sdk/runner/utils/dbConn_sync.py +485 -0
- dataops_sdk/runner/utils/duckdb_manager.py +203 -0
- dataops_sdk/runner/utils/file_transfer_base.py +307 -0
- dataops_sdk/runner/utils/ftp_file_transfer.py +242 -0
- dataops_sdk/runner/utils/log.py +50 -0
- dataops_sdk/runner/utils/nacos.py +96 -0
- dataops_sdk/runner/utils/neo4j_sync.py +236 -0
- dataops_sdk/runner/utils/queue_base.py +224 -0
- dataops_sdk/runner/utils/queue_kafka.py +150 -0
- dataops_sdk/runner/utils/sftp_file_transfer.py +243 -0
- dataops_sdk/schemas/__init__.py +32 -0
- dataops_sdk/schemas/alarm.py +38 -0
- dataops_sdk/schemas/api.py +37 -0
- dataops_sdk/schemas/data_table.py +437 -0
- dataops_sdk/schemas/datasource.py +26 -0
- dataops_sdk/schemas/dict_.py +13 -0
- dataops_sdk/schemas/enums.py +120 -0
- dataops_sdk/schemas/group.py +40 -0
- dataops_sdk/schemas/hazard.py +151 -0
- dataops_sdk/schemas/meta.py +175 -0
- dataops_sdk/schemas/query.py +175 -0
- dataops_sdk/schemas/report.py +22 -0
- dataops_sdk/schemas/skill.py +70 -0
- dataops_sdk/schemas/table.py +65 -0
- dataops_sdk/schemas/user.py +52 -0
- dataops_sdk/schemas/widget.py +651 -0
- dataops_sdk/schemas/workflow.py +63 -0
- dataops_sdk/services/__init__.py +110 -0
- dataops_sdk/services/alarm.py +324 -0
- dataops_sdk/services/api.py +28 -0
- dataops_sdk/services/auth.py +120 -0
- dataops_sdk/services/data_table.py +485 -0
- dataops_sdk/services/dataset.py +127 -0
- dataops_sdk/services/datasource.py +57 -0
- dataops_sdk/services/dict_.py +69 -0
- dataops_sdk/services/hazard.py +260 -0
- dataops_sdk/services/meta.py +523 -0
- dataops_sdk/services/metrics.py +42 -0
- dataops_sdk/services/ref.py +66 -0
- dataops_sdk/services/report.py +36 -0
- dataops_sdk/services/skill.py +276 -0
- dataops_sdk/services/table.py +340 -0
- dataops_sdk/services/widget.py +143 -0
- dataops_sdk/services/workflow.py +102 -0
- dataops_sdk/session.py +292 -0
- dataops_sdk/tiptap/__init__.py +30 -0
- dataops_sdk/tiptap/builder.py +75 -0
- dataops_sdk/tiptap/constants.py +133 -0
- dataops_sdk/tiptap/markdown_parser.py +294 -0
- dataops_sdk/tiptap/models.py +43 -0
- dataops_sdk/tiptap/parse_utils.py +889 -0
- dataops_sdk/tiptap/render_utils.py +846 -0
- dataops_sdk/tiptap/renderers.py +51 -0
- dataops_sdk/tiptap/tool.py +629 -0
- dataops_sdk/utils/__init__.py +15 -0
- dataops_sdk/utils/common.py +211 -0
- dataops_sdk/utils/utils.py +120 -0
- dataops_sdk-0.3.0.dist-info/METADATA +25 -0
- dataops_sdk-0.3.0.dist-info/RECORD +79 -0
- dataops_sdk-0.3.0.dist-info/WHEEL +5 -0
- 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''
|
|
191
|
+
else:
|
|
192
|
+
image_markdown = f""
|
|
193
|
+
else:
|
|
194
|
+
if title:
|
|
195
|
+
image_markdown = f''
|
|
196
|
+
else:
|
|
197
|
+
image_markdown = f""
|
|
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("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
|
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"
|