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,133 @@
|
|
|
1
|
+
"""TipTap constants definition.
|
|
2
|
+
|
|
3
|
+
Mirrors the Java TipTapConstants class with all attribute types, mark types,
|
|
4
|
+
markdown syntax tokens, node types, and style constants.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AttrTypes:
|
|
11
|
+
"""HTML/XML attribute names."""
|
|
12
|
+
|
|
13
|
+
LEVEL = "level"
|
|
14
|
+
CHECKED = "checked"
|
|
15
|
+
SRC = "src"
|
|
16
|
+
HREF = "href"
|
|
17
|
+
ALT = "alt"
|
|
18
|
+
TITLE = "title"
|
|
19
|
+
LANGUAGE = "language"
|
|
20
|
+
COLSPAN = "colspan"
|
|
21
|
+
ROWSPAN = "rowspan"
|
|
22
|
+
ALIGNMENT = "alignment"
|
|
23
|
+
CALLOUT_TYPE = "type"
|
|
24
|
+
CALLOUT_TITLE = "title"
|
|
25
|
+
CALLOUT_COLLAPSIBLE = "collapsible"
|
|
26
|
+
CALLOUT_COLLAPSED = "collapsed"
|
|
27
|
+
COLUMNS_COUNT = "count"
|
|
28
|
+
COLUMNS_GAP = "gap"
|
|
29
|
+
COLUMN_WIDTH = "width"
|
|
30
|
+
CODE = "code"
|
|
31
|
+
ID = "id"
|
|
32
|
+
COLUMNS = "columns"
|
|
33
|
+
GAP = "gap"
|
|
34
|
+
CHART_TYPE = "chartType"
|
|
35
|
+
ITEM_TYPE = "itemType"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MarkTypes:
|
|
39
|
+
"""Mark types for inline formatting."""
|
|
40
|
+
|
|
41
|
+
BOLD = "bold"
|
|
42
|
+
ITALIC = "italic"
|
|
43
|
+
STRIKE = "strike"
|
|
44
|
+
CODE = "code"
|
|
45
|
+
LINK = "link"
|
|
46
|
+
UNDERLINE = "underline"
|
|
47
|
+
HIGHLIGHT = "highlight"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Markdown:
|
|
51
|
+
"""Markdown syntax tokens."""
|
|
52
|
+
|
|
53
|
+
HEADING_PREFIX = "#"
|
|
54
|
+
BLOCKQUOTE_PREFIX = "> "
|
|
55
|
+
BULLET_LIST_PREFIX = "- "
|
|
56
|
+
ORDERED_LIST_PREFIX = "%d. "
|
|
57
|
+
TASK_ITEM_UNCHECKED = "[ ]"
|
|
58
|
+
TASK_ITEM_CHECKED = "[x]"
|
|
59
|
+
HORIZONTAL_RULE = "---"
|
|
60
|
+
HARD_BREAK_SUFFIX = "\n"
|
|
61
|
+
BOLD_MARKER = "**"
|
|
62
|
+
ITALIC_MARKER = "*"
|
|
63
|
+
STRIKE_MARKER = "~~"
|
|
64
|
+
CODE_MARKER = "`"
|
|
65
|
+
HIGHLIGHT_MARKER = "=="
|
|
66
|
+
UNDERLINE_HTML_OPEN = "<u>"
|
|
67
|
+
UNDERLINE_HTML_CLOSE = "</u>"
|
|
68
|
+
CODE_BLOCK_OPEN = "```"
|
|
69
|
+
CODE_BLOCK_CLOSE = "```"
|
|
70
|
+
PARAGRAPH_SEPARATOR = "\n"
|
|
71
|
+
TABLE_ROW_SEPARATOR = " | "
|
|
72
|
+
TABLE_ALIGN_SEPARATOR = "---"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class NodeTypes:
|
|
76
|
+
"""Node type constants."""
|
|
77
|
+
|
|
78
|
+
DOC = "doc"
|
|
79
|
+
PARAGRAPH = "paragraph"
|
|
80
|
+
HEADING = "heading"
|
|
81
|
+
CODE_BLOCK = "codeBlock"
|
|
82
|
+
BLOCKQUOTE = "blockquote"
|
|
83
|
+
BULLET_LIST = "bulletList"
|
|
84
|
+
ORDERED_LIST = "orderedList"
|
|
85
|
+
TASK_LIST = "taskList"
|
|
86
|
+
TASK_ITEM = "taskItem"
|
|
87
|
+
LIST_ITEM = "listItem"
|
|
88
|
+
HORIZONTAL_RULE = "horizontalRule"
|
|
89
|
+
HARD_BREAK = "hardBreak"
|
|
90
|
+
IMAGE = "image"
|
|
91
|
+
TABLE = "table"
|
|
92
|
+
TABLE_ROW = "tableRow"
|
|
93
|
+
TABLE_CELL = "tableCell"
|
|
94
|
+
TABLE_HEADER = "tableHeader"
|
|
95
|
+
TEXT = "text"
|
|
96
|
+
FIXED_TITLE = "fixedTitle"
|
|
97
|
+
WIDGET_VIEW = "widget-view"
|
|
98
|
+
WIDGET_NUM = "widget-num"
|
|
99
|
+
AI_SKILL = "ai-skill"
|
|
100
|
+
CALLOUT = "callout"
|
|
101
|
+
COLUMNS = "columns"
|
|
102
|
+
COLUMN = "column"
|
|
103
|
+
COLUMN_RESIZER = "columnResizer"
|
|
104
|
+
MENTION = "mention"
|
|
105
|
+
TABLE_DATA = "table-data"
|
|
106
|
+
DATASET_DATA = "dataset-data"
|
|
107
|
+
MERMAID = "mermaid"
|
|
108
|
+
INSIGHT_BLOCK = "ai-insight-block"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class IndentStyle(StrEnum):
|
|
112
|
+
"""Indent style options."""
|
|
113
|
+
|
|
114
|
+
SPACE = "space"
|
|
115
|
+
TAB = "tab"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TableStyle(StrEnum):
|
|
119
|
+
"""Table style options."""
|
|
120
|
+
|
|
121
|
+
MARKDOWN = "markdown"
|
|
122
|
+
HTML = "html"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Style:
|
|
126
|
+
"""Style constants."""
|
|
127
|
+
|
|
128
|
+
INDENT_STYLE_SPACE = IndentStyle.SPACE
|
|
129
|
+
INDENT_STYLE_TAB = IndentStyle.TAB
|
|
130
|
+
DEFAULT_INDENT_SIZE = 2
|
|
131
|
+
TABLE_STYLE_MARKDOWN = TableStyle.MARKDOWN
|
|
132
|
+
TABLE_STYLE_HTML = TableStyle.HTML
|
|
133
|
+
DEFAULT_TABLE_STYLE = TableStyle.HTML
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Convert Markdown tokens into the JSON structure used by Tiptap."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from markdown_it import MarkdownIt
|
|
10
|
+
|
|
11
|
+
from dataops_sdk.tiptap import constants, parse_utils
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MarkdownToTipTapParser:
|
|
15
|
+
"""Parse standard Markdown into frontend-compatible Tiptap JSON."""
|
|
16
|
+
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._markdown = MarkdownIt("default").enable("table")
|
|
19
|
+
|
|
20
|
+
def parse(self, markdown: str) -> dict:
|
|
21
|
+
"""Parse Markdown without requiring a JavaScript runtime."""
|
|
22
|
+
tokens = self._markdown.parse(markdown)
|
|
23
|
+
content, index = self._parse_blocks(tokens)
|
|
24
|
+
if index != len(tokens):
|
|
25
|
+
raise ValueError(f"Unconsumed Markdown token at index {index}: {tokens[index].type}")
|
|
26
|
+
return parse_utils.create_doc(content)
|
|
27
|
+
|
|
28
|
+
def _parse_blocks(
|
|
29
|
+
self,
|
|
30
|
+
tokens: Sequence[Any],
|
|
31
|
+
index: int = 0,
|
|
32
|
+
closing_type: str | None = None,
|
|
33
|
+
) -> tuple[list[dict], int]:
|
|
34
|
+
content: list[dict] = []
|
|
35
|
+
|
|
36
|
+
while index < len(tokens):
|
|
37
|
+
token = tokens[index]
|
|
38
|
+
if closing_type and token.type == closing_type:
|
|
39
|
+
return content, index + 1
|
|
40
|
+
|
|
41
|
+
if token.type == "paragraph_open":
|
|
42
|
+
node, index = self._parse_paragraph(tokens, index)
|
|
43
|
+
content.append(node)
|
|
44
|
+
elif token.type == "heading_open":
|
|
45
|
+
node, index = self._parse_heading(tokens, index)
|
|
46
|
+
content.append(node)
|
|
47
|
+
elif token.type == "blockquote_open":
|
|
48
|
+
children, index = self._parse_blocks(tokens, index + 1, "blockquote_close")
|
|
49
|
+
content.append(parse_utils.parse_blockquote(children))
|
|
50
|
+
elif token.type in {"bullet_list_open", "ordered_list_open"}:
|
|
51
|
+
node, index = self._parse_list(tokens, index)
|
|
52
|
+
content.append(node)
|
|
53
|
+
elif token.type == "fence":
|
|
54
|
+
content.append(self._parse_fence(token))
|
|
55
|
+
index += 1
|
|
56
|
+
elif token.type == "hr":
|
|
57
|
+
content.append(parse_utils.parse_horizontal_rule())
|
|
58
|
+
index += 1
|
|
59
|
+
elif token.type == "table_open":
|
|
60
|
+
node, index = self._parse_table(tokens, index)
|
|
61
|
+
content.append(node)
|
|
62
|
+
elif token.type == "html_block":
|
|
63
|
+
content.append(parse_utils.parse_paragraph([parse_utils.create_text_node(token.content)]))
|
|
64
|
+
index += 1
|
|
65
|
+
else:
|
|
66
|
+
raise ValueError(f"Unsupported Markdown block token: {token.type}")
|
|
67
|
+
|
|
68
|
+
if closing_type:
|
|
69
|
+
raise ValueError(f"Missing closing Markdown token: {closing_type}")
|
|
70
|
+
return content, index
|
|
71
|
+
|
|
72
|
+
def _parse_paragraph(
|
|
73
|
+
self,
|
|
74
|
+
tokens: Sequence[Any],
|
|
75
|
+
index: int,
|
|
76
|
+
) -> tuple[dict, int]:
|
|
77
|
+
inline_token = self._require_token(tokens, index + 1, "inline")
|
|
78
|
+
content = self._parse_inline(inline_token.children or [])
|
|
79
|
+
close_index = index + 2
|
|
80
|
+
self._require_token(tokens, close_index, "paragraph_close")
|
|
81
|
+
|
|
82
|
+
if len(content) == 1 and content[0].get("type") == constants.NodeTypes.IMAGE:
|
|
83
|
+
return content[0], close_index + 1
|
|
84
|
+
return parse_utils.parse_paragraph(content), close_index + 1
|
|
85
|
+
|
|
86
|
+
def _parse_heading(
|
|
87
|
+
self,
|
|
88
|
+
tokens: Sequence[Any],
|
|
89
|
+
index: int,
|
|
90
|
+
) -> tuple[dict, int]:
|
|
91
|
+
inline_token = self._require_token(tokens, index + 1, "inline")
|
|
92
|
+
content = self._parse_inline(inline_token.children or [])
|
|
93
|
+
close_index = index + 2
|
|
94
|
+
self._require_token(tokens, close_index, "heading_close")
|
|
95
|
+
level = int(tokens[index].tag[1:])
|
|
96
|
+
return parse_utils.parse_heading(content, level), close_index + 1
|
|
97
|
+
|
|
98
|
+
def _parse_list(
|
|
99
|
+
self,
|
|
100
|
+
tokens: Sequence[Any],
|
|
101
|
+
index: int,
|
|
102
|
+
) -> tuple[dict, int]:
|
|
103
|
+
opening = tokens[index]
|
|
104
|
+
closing_type = (
|
|
105
|
+
"bullet_list_close" if opening.type == "bullet_list_open" else "ordered_list_close"
|
|
106
|
+
)
|
|
107
|
+
items: list[tuple[dict, bool]] = []
|
|
108
|
+
index += 1
|
|
109
|
+
|
|
110
|
+
while index < len(tokens) and tokens[index].type != closing_type:
|
|
111
|
+
self._require_token(tokens, index, "list_item_open")
|
|
112
|
+
item_content, index = self._parse_blocks(tokens, index + 1, "list_item_close")
|
|
113
|
+
if not item_content:
|
|
114
|
+
raise ValueError("Markdown list item has no content")
|
|
115
|
+
task_item = self._convert_task_item(item_content[0])
|
|
116
|
+
if task_item is not None:
|
|
117
|
+
task_item["content"] = item_content
|
|
118
|
+
items.append((task_item, True))
|
|
119
|
+
else:
|
|
120
|
+
items.append((parse_utils.parse_list_item(item_content), False))
|
|
121
|
+
|
|
122
|
+
self._require_token(tokens, index, closing_type)
|
|
123
|
+
index += 1
|
|
124
|
+
|
|
125
|
+
if items and all(is_task for _, is_task in items):
|
|
126
|
+
return parse_utils.parse_task_list([item for item, _ in items]), index
|
|
127
|
+
|
|
128
|
+
attrs: dict[str, int] | None = None
|
|
129
|
+
if opening.type == "ordered_list_open" and opening.attrs.get("start"):
|
|
130
|
+
attrs = {"start": opening.attrs["start"]}
|
|
131
|
+
node_type = (
|
|
132
|
+
constants.NodeTypes.BULLET_LIST
|
|
133
|
+
if opening.type == "bullet_list_open"
|
|
134
|
+
else constants.NodeTypes.ORDERED_LIST
|
|
135
|
+
)
|
|
136
|
+
return parse_utils.create_node(node_type, attrs=attrs, content=[item for item, _ in items]), index
|
|
137
|
+
|
|
138
|
+
def _convert_task_item(self, paragraph: dict) -> dict | None:
|
|
139
|
+
if paragraph.get("type") != constants.NodeTypes.PARAGRAPH:
|
|
140
|
+
return None
|
|
141
|
+
paragraph_content = paragraph.get("content") or []
|
|
142
|
+
if not paragraph_content or paragraph_content[0].get("type") != constants.NodeTypes.TEXT:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
first_text = paragraph_content[0].get("text", "")
|
|
146
|
+
match = re.match(r"^\[([ xX])\]\s+", first_text)
|
|
147
|
+
if not match:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
paragraph_content[0]["text"] = first_text[match.end() :]
|
|
151
|
+
if not paragraph_content[0]["text"]:
|
|
152
|
+
paragraph_content.pop(0)
|
|
153
|
+
checked = match.group(1).lower() == "x"
|
|
154
|
+
return parse_utils.create_node(
|
|
155
|
+
constants.NodeTypes.TASK_ITEM,
|
|
156
|
+
attrs={constants.AttrTypes.CHECKED: checked},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _parse_fence(self, token: Any) -> dict:
|
|
160
|
+
language = token.info.strip().split(maxsplit=1)[0] if token.info.strip() else None
|
|
161
|
+
return parse_utils.parse_code_block(token.content.rstrip("\n"), language)
|
|
162
|
+
|
|
163
|
+
def _parse_table(
|
|
164
|
+
self,
|
|
165
|
+
tokens: Sequence[Any],
|
|
166
|
+
index: int,
|
|
167
|
+
) -> tuple[dict, int]:
|
|
168
|
+
rows: list[dict] = []
|
|
169
|
+
index += 1
|
|
170
|
+
while index < len(tokens) and tokens[index].type != "table_close":
|
|
171
|
+
if tokens[index].type in {"thead_open", "tbody_open"}:
|
|
172
|
+
index += 1
|
|
173
|
+
continue
|
|
174
|
+
if tokens[index].type in {"thead_close", "tbody_close"}:
|
|
175
|
+
index += 1
|
|
176
|
+
continue
|
|
177
|
+
if tokens[index].type != "tr_open":
|
|
178
|
+
raise ValueError(f"Unexpected table token: {tokens[index].type}")
|
|
179
|
+
row, index = self._parse_table_row(tokens, index)
|
|
180
|
+
rows.append(row)
|
|
181
|
+
self._require_token(tokens, index, "table_close")
|
|
182
|
+
return parse_utils.parse_table(rows), index + 1
|
|
183
|
+
|
|
184
|
+
def _parse_table_row(
|
|
185
|
+
self,
|
|
186
|
+
tokens: Sequence[Any],
|
|
187
|
+
index: int,
|
|
188
|
+
) -> tuple[dict, int]:
|
|
189
|
+
cells: list[dict] = []
|
|
190
|
+
index += 1
|
|
191
|
+
while index < len(tokens) and tokens[index].type != "tr_close":
|
|
192
|
+
cell_token = tokens[index]
|
|
193
|
+
if cell_token.type not in {"th_open", "td_open"}:
|
|
194
|
+
raise ValueError(f"Unexpected table cell token: {cell_token.type}")
|
|
195
|
+
inline_token = self._require_token(tokens, index + 1, "inline")
|
|
196
|
+
paragraph = parse_utils.parse_paragraph(self._parse_inline(inline_token.children or []))
|
|
197
|
+
cell_type = (
|
|
198
|
+
constants.NodeTypes.TABLE_HEADER
|
|
199
|
+
if cell_token.type == "th_open"
|
|
200
|
+
else constants.NodeTypes.TABLE_CELL
|
|
201
|
+
)
|
|
202
|
+
cells.append(parse_utils.create_node(cell_type, content=[paragraph]))
|
|
203
|
+
close_type = "th_close" if cell_token.type == "th_open" else "td_close"
|
|
204
|
+
self._require_token(tokens, index + 2, close_type)
|
|
205
|
+
index += 3
|
|
206
|
+
self._require_token(tokens, index, "tr_close")
|
|
207
|
+
return parse_utils.parse_table_row(cells), index + 1
|
|
208
|
+
|
|
209
|
+
def _parse_inline(self, tokens: Sequence[Any]) -> list[dict]:
|
|
210
|
+
result: list[dict] = []
|
|
211
|
+
marks: list[dict] = []
|
|
212
|
+
for token in tokens:
|
|
213
|
+
if token.type.endswith("_open"):
|
|
214
|
+
mark = self._mark_for_open_token(token)
|
|
215
|
+
if mark is not None:
|
|
216
|
+
marks.append(mark)
|
|
217
|
+
continue
|
|
218
|
+
if token.type.endswith("_close"):
|
|
219
|
+
self._close_mark(marks, token)
|
|
220
|
+
continue
|
|
221
|
+
if token.type in {"text", "softbreak"}:
|
|
222
|
+
text = token.content if token.type == "text" else "\n"
|
|
223
|
+
if text:
|
|
224
|
+
result.append(parse_utils.create_text_node(text, list(marks) or None))
|
|
225
|
+
continue
|
|
226
|
+
if token.type == "code_inline":
|
|
227
|
+
result.append(
|
|
228
|
+
parse_utils.create_text_node(
|
|
229
|
+
token.content,
|
|
230
|
+
[*marks, {"type": constants.MarkTypes.CODE}],
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
continue
|
|
234
|
+
if token.type == "hardbreak":
|
|
235
|
+
result.append(parse_utils.parse_hard_break())
|
|
236
|
+
continue
|
|
237
|
+
if token.type == "image":
|
|
238
|
+
result.append(
|
|
239
|
+
parse_utils.create_node(
|
|
240
|
+
constants.NodeTypes.IMAGE,
|
|
241
|
+
attrs={
|
|
242
|
+
constants.AttrTypes.SRC: token.attrs.get("src", ""),
|
|
243
|
+
constants.AttrTypes.TITLE: token.attrs.get("title"),
|
|
244
|
+
constants.AttrTypes.ALT: token.content,
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
continue
|
|
249
|
+
if token.type == "html_inline":
|
|
250
|
+
result.append(parse_utils.create_text_node(token.content, list(marks) or None))
|
|
251
|
+
continue
|
|
252
|
+
raise ValueError(f"Unsupported Markdown inline token: {token.type}")
|
|
253
|
+
return result
|
|
254
|
+
|
|
255
|
+
def _mark_for_open_token(self, token: Any) -> dict | None:
|
|
256
|
+
mark_types = {
|
|
257
|
+
"strong_open": constants.MarkTypes.BOLD,
|
|
258
|
+
"em_open": constants.MarkTypes.ITALIC,
|
|
259
|
+
"s_open": constants.MarkTypes.STRIKE,
|
|
260
|
+
}
|
|
261
|
+
if token.type in mark_types:
|
|
262
|
+
return {"type": mark_types[token.type]}
|
|
263
|
+
if token.type == "link_open":
|
|
264
|
+
return {
|
|
265
|
+
"type": constants.MarkTypes.LINK,
|
|
266
|
+
"attrs": {
|
|
267
|
+
constants.AttrTypes.HREF: token.attrs.get("href", ""),
|
|
268
|
+
constants.AttrTypes.TITLE: token.attrs.get("title"),
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
def _close_mark(self, marks: list[dict], token: Any) -> None:
|
|
274
|
+
close_types = {
|
|
275
|
+
"strong_close": constants.MarkTypes.BOLD,
|
|
276
|
+
"em_close": constants.MarkTypes.ITALIC,
|
|
277
|
+
"s_close": constants.MarkTypes.STRIKE,
|
|
278
|
+
"link_close": constants.MarkTypes.LINK,
|
|
279
|
+
}
|
|
280
|
+
mark_type = close_types.get(token.type)
|
|
281
|
+
if mark_type is None:
|
|
282
|
+
return
|
|
283
|
+
for position in range(len(marks) - 1, -1, -1):
|
|
284
|
+
if marks[position]["type"] == mark_type:
|
|
285
|
+
marks.pop(position)
|
|
286
|
+
return
|
|
287
|
+
raise ValueError(f"Markdown mark closed without opening token: {token.type}")
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def _require_token(tokens: Sequence[Any], index: int, token_type: str) -> Any:
|
|
291
|
+
if index >= len(tokens) or tokens[index].type != token_type:
|
|
292
|
+
actual = tokens[index].type if index < len(tokens) else "end of input"
|
|
293
|
+
raise ValueError(f"Expected Markdown token {token_type}, got {actual}")
|
|
294
|
+
return tokens[index]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""TipTap data models.
|
|
2
|
+
|
|
3
|
+
Pydantic models for TipTap JSON structure.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Mark(BaseModel):
|
|
12
|
+
"""A mark applied to text (bold, italic, etc.)."""
|
|
13
|
+
|
|
14
|
+
type: str
|
|
15
|
+
attrs: dict[str, Any] | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TextNode(BaseModel):
|
|
19
|
+
"""Text node with optional marks."""
|
|
20
|
+
|
|
21
|
+
type: Literal["text"] = "text"
|
|
22
|
+
text: str
|
|
23
|
+
marks: list[Mark] | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Node(BaseModel):
|
|
27
|
+
"""Generic TipTap node."""
|
|
28
|
+
|
|
29
|
+
type: str
|
|
30
|
+
attrs: dict[str, Any] | None = None
|
|
31
|
+
content: list["Node"] | None = None
|
|
32
|
+
marks: list[Mark] | None = None # For text nodes
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DocNode(BaseModel):
|
|
36
|
+
"""Document root node."""
|
|
37
|
+
|
|
38
|
+
type: Literal["doc"] = "doc"
|
|
39
|
+
content: list[Node] | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Allow recursive model reference
|
|
43
|
+
Node.model_rebuild()
|