OVAPortableText 0.1.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.
@@ -0,0 +1,426 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Text-layer models for OVAPortableText.
5
+ OVAPortableText 的文本层模型。
6
+
7
+ This module is intentionally a little richer in Step 5.
8
+ 第 5 步开始,本模块会比前几步更完整一些。
9
+
10
+ Why is this module important?
11
+ 为什么这个模块很重要?
12
+ Because most report-writing code spends most of its time here:
13
+ 因为大多数“写报告”的代码,绝大部分时间都在操作这一层:
14
+ - paragraph-like text blocks / 类段落文本块
15
+ - inline spans / 行内文本片段
16
+ - decorators and annotations / 装饰 marks 与 annotation
17
+ - list items / 列表项
18
+
19
+ Design choice in Step 5 / 第 5 步的设计选择:
20
+ 1. Keep formal document structure in `Section`, not in text styles.
21
+ 正式文档结构仍由 `Section` 表达,而不是依赖文本 style。
22
+ 2. Reuse Portable Text native ideas for `marks / markDefs / listItem / level`.
23
+ 沿用 Portable Text 原生思路表达 `marks / markDefs / listItem / level`。
24
+ 3. Keep the Python API ergonomic for manual report construction.
25
+ 让 Python 端手写报告对象时仍然保持顺手。
26
+ """
27
+
28
+ from typing import Any, Literal, TypeAlias
29
+
30
+ from pydantic import Field, field_validator, model_validator
31
+
32
+ from .base import OvaBaseModel
33
+ from .inline import CitationRef, FootnoteRef, GlossaryTerm, HardBreak, InlineObject, XRef
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Text styles / 文本样式
37
+ # ---------------------------------------------------------------------------
38
+ # These styles are the protocol-approved styles currently allowed inside
39
+ # `content.blocks[]`, `footnotes[].blocks`, and `callout.blocks`.
40
+ # 这些是当前协议批准的文本样式,可用于
41
+ # `content.blocks[]`、`footnotes[].blocks`、`callout.blocks`。
42
+ ALLOWED_TEXT_STYLES = {
43
+ "normal",
44
+ "blockquote",
45
+ "caption",
46
+ "figure_caption",
47
+ "table_caption",
48
+ "equation_caption",
49
+ "smallprint",
50
+ "lead",
51
+ "quote_source",
52
+ "subheading",
53
+ }
54
+
55
+ TextStyle: TypeAlias = Literal[
56
+ "normal",
57
+ "blockquote",
58
+ "caption",
59
+ "figure_caption",
60
+ "table_caption",
61
+ "equation_caption",
62
+ "smallprint",
63
+ "lead",
64
+ "quote_source",
65
+ "subheading",
66
+ ]
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Marks / 标记
70
+ # ---------------------------------------------------------------------------
71
+ # Portable Text spans use `marks: list[str]`.
72
+ # Portable Text 的 span 通过 `marks: list[str]` 表达装饰与 annotation 引用。
73
+ # Some marks are plain decorators, such as `strong` or `em`.
74
+ # 有些 marks 是简单装饰器,例如 `strong` 或 `em`。
75
+ # Some marks are annotation references, which point to entries in `markDefs[]`.
76
+ # 有些 marks 是 annotation 引用,它们会指向 `markDefs[]` 中的定义对象。
77
+ ALLOWED_DECORATOR_MARKS = {"strong", "em", "underline", "code"}
78
+
79
+ DecoratorMark: TypeAlias = Literal["strong", "em", "underline", "code"]
80
+ ListItemStyle: TypeAlias = Literal["bullet", "number"]
81
+
82
+
83
+ class MarkDefBase(OvaBaseModel):
84
+ """
85
+ Common base for one mark-definition entry inside `block.markDefs[]`.
86
+ `block.markDefs[]` 中单个 mark 定义对象的公共基类。
87
+
88
+ Portable Text convention / Portable Text 约定:
89
+ - `_key` is referenced by a span's `marks[]`
90
+ `_key` 会被 span 的 `marks[]` 引用
91
+ - `_type` describes the annotation kind
92
+ `_type` 描述 annotation 类型
93
+
94
+ Important distinction / 重要区分:
95
+ decorators like `strong` do NOT live inside `markDefs[]`.
96
+ `strong` 这类装饰器并不放在 `markDefs[]` 里。
97
+ Only annotation-like definitions live there.
98
+ 只有 annotation 风格的定义对象才放进这里。
99
+ """
100
+
101
+ key: str = Field(alias="_key", serialization_alias="_key")
102
+ type_: str = Field(alias="_type", serialization_alias="_type")
103
+
104
+
105
+ class LinkMarkDef(MarkDefBase):
106
+ """
107
+ Built-in link annotation definition.
108
+ 内置的链接 annotation 定义。
109
+
110
+ This corresponds to a span mark that references this definition by `_key`.
111
+ 它会被某个 span 的 `marks[]` 通过 `_key` 进行引用。
112
+
113
+ Example / 示例:
114
+ - span marks: ["m-link-1"]
115
+ span marks: ["m-link-1"]
116
+ - markDefs: [{"_key": "m-link-1", "_type": "link", "href": "..."}]
117
+ markDefs: [{"_key": "m-link-1", "_type": "link", "href": "..."}]
118
+ """
119
+
120
+ type_: Literal["link"] = Field(default="link", alias="_type", serialization_alias="_type")
121
+ href: str
122
+ title: str | None = None
123
+ openInNewTab: bool | None = None
124
+ rel: str | None = None
125
+
126
+
127
+ class AnnotationMarkDef(MarkDefBase):
128
+ """
129
+ Generic custom annotation definition.
130
+ 通用自定义 annotation 定义。
131
+
132
+ Why keep this generic?
133
+ 为什么把它设计成通用型?
134
+ Because the protocol mentions that `markDefs` can also carry comments,
135
+ notes, and future annotation-like semantics, not just links.
136
+ 因为协议提到 `markDefs` 不仅可以承载链接,还可以承载批注、注释
137
+ 以及未来其他 annotation 语义。
138
+
139
+ `data` is an open object bucket for annotation payload.
140
+ `data` 是 annotation 载荷的开放对象桶。
141
+ """
142
+
143
+ data: dict[str, Any] = Field(default_factory=dict)
144
+
145
+
146
+ MarkDef: TypeAlias = LinkMarkDef | AnnotationMarkDef
147
+ """
148
+ Union of currently supported mark-definition models.
149
+ 当前支持的 mark-definition 模型联合类型。
150
+ """
151
+
152
+
153
+ class Span(OvaBaseModel):
154
+ """
155
+ Plain inline text span.
156
+ 普通行内文本片段。
157
+
158
+ Notes / 说明:
159
+ 1. `_type="span"` follows Portable Text native convention.
160
+ `_type="span"` 沿用 Portable Text 原生约定。
161
+ 2. `marks` stores both decorator marks and markDef keys.
162
+ `marks` 同时承载装饰器 mark 与 markDef 的 key。
163
+ 3. This means `marks=["strong", "m-link-1"]` is valid.
164
+ 也就是说 `marks=["strong", "m-link-1"]` 是合法的。
165
+ """
166
+
167
+ type_: Literal["span"] = Field(default="span", alias="_type", serialization_alias="_type")
168
+ text: str
169
+ marks: list[str] = Field(default_factory=list)
170
+
171
+ def add_mark(self, mark: str) -> "Span":
172
+ """
173
+ Append one mark string to the span.
174
+ 向当前 span 追加一个 mark 字符串。
175
+
176
+ This method does not try to interpret whether the mark is a decorator
177
+ or a markDef key. That resolution belongs to the containing block.
178
+ 这个方法不会判断该 mark 是装饰器还是 markDef key;
179
+ 真正的解释工作应由包含它的 block 完成。
180
+ """
181
+ self.marks.append(mark)
182
+ return self
183
+
184
+
185
+ TextChild: TypeAlias = Span | HardBreak | XRef | CitationRef | FootnoteRef | GlossaryTerm
186
+ """
187
+ All currently supported child elements inside `block.children[]`.
188
+ 当前 `block.children[]` 支持的全部子元素类型。
189
+ """
190
+
191
+
192
+ class TextBlock(OvaBaseModel):
193
+ """
194
+ Portable Text style text block.
195
+ Portable Text 风格文本块。
196
+
197
+ This is the most common block type in report writing.
198
+ 这是写报告时最常见的一类块。
199
+
200
+ It now supports three major Portable Text abilities:
201
+ 现在它支持三组很重要的 Portable Text 原生能力:
202
+ 1. `children[]` for inline sequence
203
+ 通过 `children[]` 表达行内顺序
204
+ 2. `marks / markDefs` for decorators and annotations
205
+ 通过 `marks / markDefs` 表达装饰与 annotation
206
+ 3. `listItem / level` for list semantics
207
+ 通过 `listItem / level` 表达列表语义
208
+
209
+ Important boundary / 重要边界:
210
+ formal document headings are still represented by `Section`, not by `h1/h2`.
211
+ 正式文档标题仍由 `Section` 表达,而不是使用 `h1/h2`。
212
+ """
213
+
214
+ type_: Literal["block"] = Field(default="block", alias="_type", serialization_alias="_type")
215
+ style: TextStyle = "normal"
216
+ children: list[TextChild] = Field(default_factory=list)
217
+ markDefs: list[MarkDef] = Field(default_factory=list)
218
+ listItem: ListItemStyle | None = None
219
+ level: int | None = None
220
+
221
+ @field_validator("style")
222
+ @classmethod
223
+ def validate_style(cls, value: str) -> str:
224
+ """
225
+ Restrict style to the protocol-approved set.
226
+ 将 style 限制在协议批准的范围内。
227
+ """
228
+ if value not in ALLOWED_TEXT_STYLES:
229
+ allowed = ", ".join(sorted(ALLOWED_TEXT_STYLES))
230
+ raise ValueError(f"Unsupported text block style: {value!r}. Allowed styles: {allowed}")
231
+ return value
232
+
233
+ @field_validator("markDefs")
234
+ @classmethod
235
+ def validate_mark_def_keys(cls, value: list[MarkDef]) -> list[MarkDef]:
236
+ """
237
+ Require unique `_key` values inside one block.
238
+ 要求同一个 block 内 `markDefs[]` 的 `_key` 唯一。
239
+
240
+ Why?
241
+ 为什么要这样?
242
+ Because spans resolve annotation marks by key.
243
+ 因为 span 会通过 key 来解析 annotation mark。
244
+ If keys collide, resolution becomes ambiguous.
245
+ 如果 key 冲突,解析就会产生歧义。
246
+ """
247
+ seen: set[str] = set()
248
+ for item in value:
249
+ if item.key in seen:
250
+ raise ValueError(f"Duplicate markDef key in the same block: {item.key}")
251
+ seen.add(item.key)
252
+ return value
253
+
254
+ @model_validator(mode="after")
255
+ def validate_list_semantics(self) -> "TextBlock":
256
+ """
257
+ Normalize and validate list semantics.
258
+ 规范化并校验列表语义。
259
+
260
+ Rules / 规则:
261
+ 1. If `listItem` is present but `level` is omitted, default to 1.
262
+ 若存在 `listItem` 但未提供 `level`,默认补成 1。
263
+ 2. `level` must be >= 1 when present.
264
+ 当 `level` 存在时,必须 >= 1。
265
+ 3. `level` must not appear without `listItem`.
266
+ 不能只给 `level` 而不给 `listItem`。
267
+ """
268
+ if self.listItem is not None and self.level is None:
269
+ self.level = 1
270
+
271
+ if self.level is not None and self.level < 1:
272
+ raise ValueError("Text block list level must be >= 1")
273
+
274
+ if self.listItem is None and self.level is not None:
275
+ raise ValueError("`level` cannot appear without `listItem`")
276
+
277
+ return self
278
+
279
+ @classmethod
280
+ def from_parts(
281
+ cls,
282
+ *parts: str | TextChild,
283
+ style: TextStyle = "normal",
284
+ mark_defs: list[MarkDef] | None = None,
285
+ list_item: ListItemStyle | None = None,
286
+ level: int | None = None,
287
+ ) -> "TextBlock":
288
+ """
289
+ Create a block from mixed text and inline objects.
290
+ 用混合的文本与行内对象构造一个 block。
291
+
292
+ Strings are automatically converted into `Span`.
293
+ 普通字符串会自动转换成 `Span`。
294
+
295
+ This is the main convenience constructor used by helper functions,
296
+ `Section.append_paragraph`, list-item APIs, and example scripts.
297
+ 这是 helper 函数、`Section.append_paragraph`、列表 API、示例脚本
298
+ 等场景最常用的便捷构造入口。
299
+ """
300
+ block = cls(style=style, markDefs=mark_defs or [], listItem=list_item, level=level)
301
+ for part in parts:
302
+ block.append(part)
303
+ return block
304
+
305
+ @classmethod
306
+ def paragraph(
307
+ cls,
308
+ text: str,
309
+ *,
310
+ style: TextStyle = "normal",
311
+ mark_defs: list[MarkDef] | None = None,
312
+ ) -> "TextBlock":
313
+ """
314
+ Quick constructor for a plain one-string paragraph.
315
+ 单字符串段落的快捷构造器。
316
+ """
317
+ return cls.from_parts(text, style=style, mark_defs=mark_defs)
318
+
319
+ @classmethod
320
+ def list_block(
321
+ cls,
322
+ *parts: str | TextChild,
323
+ list_item: ListItemStyle = "bullet",
324
+ level: int = 1,
325
+ style: TextStyle = "normal",
326
+ mark_defs: list[MarkDef] | None = None,
327
+ ) -> "TextBlock":
328
+ """
329
+ Convenience constructor for one list item block.
330
+ 单个列表项 block 的便捷构造器。
331
+
332
+ Even though it is called `list_block`, it is still a normal Portable
333
+ Text `block` at the JSON level. The list semantics come from
334
+ `listItem` + `level`.
335
+ 虽然这里叫 `list_block`,但在 JSON 层它仍然是普通 Portable Text
336
+ 的 `block`;列表语义来自 `listItem` + `level`。
337
+ """
338
+ return cls.from_parts(
339
+ *parts,
340
+ style=style,
341
+ mark_defs=mark_defs,
342
+ list_item=list_item,
343
+ level=level,
344
+ )
345
+
346
+ def append(self, part: str | TextChild) -> "TextBlock":
347
+ """
348
+ Append one child into the block.
349
+ 向当前 block 追加一个子元素。
350
+
351
+ Args / 参数:
352
+ part:
353
+ A plain string or a supported inline object.
354
+ 可以是普通字符串,也可以是已支持的行内对象。
355
+ """
356
+ if isinstance(part, str):
357
+ self.children.append(Span(text=part))
358
+ else:
359
+ self.children.append(part)
360
+ return self
361
+
362
+ def append_text(self, text: str, *, marks: list[str] | None = None) -> "TextBlock":
363
+ """
364
+ Append plain text as a `Span`.
365
+ 以 `Span` 的形式追加普通文本。
366
+
367
+ This is especially useful when you want to explicitly attach marks.
368
+ 当你希望显式绑定 marks 时,这个方法尤其方便。
369
+ """
370
+ self.children.append(Span(text=text, marks=marks or []))
371
+ return self
372
+
373
+ def append_inline(self, inline: InlineObject) -> "TextBlock":
374
+ """
375
+ Append a supported inline object.
376
+ 追加一个已支持的行内对象。
377
+ """
378
+ self.children.append(inline)
379
+ return self
380
+
381
+ def add_mark_def(self, mark_def: MarkDef) -> "TextBlock":
382
+ """
383
+ Append one mark definition into `markDefs[]`.
384
+ 向 `markDefs[]` 追加一个 mark 定义对象。
385
+ """
386
+ self.markDefs.append(mark_def)
387
+ return self
388
+
389
+ def add_link_def(
390
+ self,
391
+ *,
392
+ key: str,
393
+ href: str,
394
+ title: str | None = None,
395
+ open_in_new_tab: bool | None = None,
396
+ rel: str | None = None,
397
+ ) -> "TextBlock":
398
+ """
399
+ Append one `link` mark definition into `markDefs[]`.
400
+ 向 `markDefs[]` 追加一个 `link` mark 定义。
401
+
402
+ The caller still needs to reference this mark key from one or more spans.
403
+ 调用方仍需要在一个或多个 span 的 `marks[]` 中引用这个 key。
404
+ """
405
+ self.markDefs.append(
406
+ LinkMarkDef(
407
+ _key=key,
408
+ href=href,
409
+ title=title,
410
+ openInNewTab=open_in_new_tab,
411
+ rel=rel,
412
+ )
413
+ )
414
+ return self
415
+
416
+ def set_list(self, *, list_item: ListItemStyle, level: int = 1) -> "TextBlock":
417
+ """
418
+ Convert the current block into a list item block.
419
+ 把当前 block 转成一个列表项 block。
420
+
421
+ This mutates the current object and returns itself for chaining.
422
+ 这个方法会原地修改当前对象,并返回自身以便链式调用。
423
+ """
424
+ self.listItem = list_item
425
+ self.level = level
426
+ return self
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Theme-related models for OVAPortableText.
5
+ OVAPortableText 的主题相关模型。
6
+
7
+ The protocol explicitly says `theme` is currently only a placeholder and should
8
+ not be over-specified yet.
9
+ 协议明确指出当前版本的 `theme` 仍是占位层,不应过度细化。
10
+
11
+ So this module provides:
12
+ 因此本模块提供:
13
+ 1. a small typed core for the most obvious fields
14
+ 一小组最明显的强类型字段
15
+ 2. extension-friendly behaviour for future renderer-specific additions
16
+ 面向未来渲染器扩展的宽松扩展能力
17
+ """
18
+
19
+ from pydantic import ConfigDict
20
+
21
+ from .base import OvaBaseModel
22
+
23
+
24
+ class ThemeConfig(OvaBaseModel):
25
+ """
26
+ Lightweight typed model for the top-level `theme` object.
27
+ 顶层 `theme` 对象的轻量强类型模型。
28
+
29
+ The protocol currently treats `theme` as a placeholder.
30
+ 协议当前把 `theme` 视作占位层。
31
+
32
+ Therefore this model is intentionally permissive:
33
+ 因此这个模型刻意保持较宽松:
34
+ - it gives common fields typed names
35
+ 为常见字段提供强类型名字
36
+ - it still allows extra keys
37
+ 但仍允许额外字段存在
38
+ """
39
+
40
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
41
+
42
+ name: str | None = None
43
+ styleTemplate: str | None = None
44
+ pageTemplateFamily: str | None = None
45
+ brandAssetRefs: list[str] | None = None
46
+ coverTemplateRef: str | None = None