docx-cli 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.
- docx_cli/__init__.py +8 -0
- docx_cli/__main__.py +7 -0
- docx_cli/_templates/hodatajstvo.json +60 -0
- docx_cli/_templates/iskovoe_zayavlenie.json +75 -0
- docx_cli/_templates/otzyv_na_isk.json +69 -0
- docx_cli/_templates/soyu_rayon.json +61 -0
- docx_cli/_templates/vs_sud.json +65 -0
- docx_cli/agent_reader.py +87 -0
- docx_cli/blocks.py +333 -0
- docx_cli/builder.py +116 -0
- docx_cli/cli.py +641 -0
- docx_cli/errors.py +21 -0
- docx_cli/fonts_normalize.py +119 -0
- docx_cli/inject_scan.py +369 -0
- docx_cli/mcp_server.py +380 -0
- docx_cli/md_court.py +665 -0
- docx_cli/profiles.py +272 -0
- docx_cli/spec.py +233 -0
- docx_cli/validate.py +786 -0
- docx_cli-0.1.0.dist-info/METADATA +95 -0
- docx_cli-0.1.0.dist-info/RECORD +25 -0
- docx_cli-0.1.0.dist-info/WHEEL +4 -0
- docx_cli-0.1.0.dist-info/entry_points.txt +3 -0
- docx_cli-0.1.0.dist-info/licenses/LICENSE +199 -0
- docx_cli-0.1.0.dist-info/licenses/NOTICE +1 -0
docx_cli/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""docx-cli — deterministic .docx generation for Russian legal documents."""
|
|
2
|
+
from docx_cli.builder import build_document
|
|
3
|
+
from docx_cli.spec import DocSpec
|
|
4
|
+
from docx_cli.validate import validate_document
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
__all__ = ["build_document", "DocSpec", "validate_document"]
|
docx_cli/__main__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"profile": "court",
|
|
3
|
+
"output": {
|
|
4
|
+
"file_name": "{дд.мм.гггг}.Ходатайство_{предмет_кратко}.docx",
|
|
5
|
+
"author": "{фио_представителя}"
|
|
6
|
+
},
|
|
7
|
+
"header": {
|
|
8
|
+
"align": "right",
|
|
9
|
+
"lines": [
|
|
10
|
+
"В {наименование_суда}",
|
|
11
|
+
"Судья: {фио_судьи}",
|
|
12
|
+
"Дело № {номер_дела}",
|
|
13
|
+
"Заявитель: {заявитель_наименование}, ИНН {инн}, ОГРН {огрн}"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"title": {
|
|
17
|
+
"main": "ХОДАТАЙСТВО",
|
|
18
|
+
"subtitle": "{предмет_ходатайства: о назначении экспертизы / об истребовании доказательств / об обеспечении иска / о восстановлении срока}"
|
|
19
|
+
},
|
|
20
|
+
"body": [
|
|
21
|
+
{
|
|
22
|
+
"type": "paragraph",
|
|
23
|
+
"text": "В производстве суда находится дело № {номер_дела} по иску {истец_наименование} к {ответчик_наименование}.",
|
|
24
|
+
"inline_markdown": false
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "paragraph",
|
|
28
|
+
"text": "{обстоятельства_заявления}",
|
|
29
|
+
"inline_markdown": true
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "paragraph",
|
|
33
|
+
"text": "Правовое основание заявленного ходатайства: {правовое_обоснование} (процессуальный институт — {статья_АПК_или_ГПК}). Заявленное ходатайство отвечает требованиям: {критерии_удовлетворения}.",
|
|
34
|
+
"inline_markdown": true
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "paragraph",
|
|
38
|
+
"text": "На основании изложенного, руководствуясь {статья_АПК_или_ГПК}, прошу:",
|
|
39
|
+
"inline_markdown": false
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "numbered_list",
|
|
43
|
+
"items": [
|
|
44
|
+
"{просительная_часть}"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"type": "attachments_list",
|
|
49
|
+
"items": [
|
|
50
|
+
"{приложение_1}",
|
|
51
|
+
"документ о полномочиях представителя (доверенность) — ст. 61, 62 АПК РФ{приложение_2}"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"type": "signature_block",
|
|
56
|
+
"left": "Дата: {дата}",
|
|
57
|
+
"right": "Представитель _________________ /{фио_представителя}/"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"profile": "court",
|
|
3
|
+
"output": {
|
|
4
|
+
"file_name": "{дд.мм.гггг}.Исковое_заявление.docx",
|
|
5
|
+
"author": "{фио_представителя}"
|
|
6
|
+
},
|
|
7
|
+
"header": {
|
|
8
|
+
"align": "right",
|
|
9
|
+
"lines": [
|
|
10
|
+
"В {наименование_суда}",
|
|
11
|
+
"Истец: {истец_наименование}, ИНН {инн}, ОГРН {огрн}",
|
|
12
|
+
"Адрес: {адрес_истца}",
|
|
13
|
+
"Ответчик: {ответчик_наименование}, ИНН {инн_ответчика}, ОГРН {огрн_ответчика}",
|
|
14
|
+
"Адрес: {адрес_ответчика}",
|
|
15
|
+
"Цена иска: {цена_иска} руб.",
|
|
16
|
+
"Госпошлина: {госпошлина} руб. (расчет по ст. 333.21 НК РФ)"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"title": {
|
|
20
|
+
"main": "ИСКОВОЕ ЗАЯВЛЕНИЕ",
|
|
21
|
+
"subtitle": "о взыскании {предмет_иска}"
|
|
22
|
+
},
|
|
23
|
+
"body": [
|
|
24
|
+
{
|
|
25
|
+
"type": "paragraph",
|
|
26
|
+
"text": "{обстоятельства_дела}",
|
|
27
|
+
"inline_markdown": true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "paragraph",
|
|
31
|
+
"text": "Правовое обоснование: {правовое_обоснование}",
|
|
32
|
+
"inline_markdown": true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "paragraph",
|
|
36
|
+
"text": "Расчет взыскиваемой суммы:",
|
|
37
|
+
"inline_markdown": false
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"type": "numbered_list",
|
|
41
|
+
"items": [
|
|
42
|
+
"{расчет_1}",
|
|
43
|
+
"{расчет_2}"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"type": "paragraph",
|
|
48
|
+
"text": "На основании изложенного, руководствуясь ст. 125, 126 АПК РФ, прошу:",
|
|
49
|
+
"inline_markdown": false
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "numbered_list",
|
|
53
|
+
"items": [
|
|
54
|
+
"взыскать с Ответчика {требование_1} (нормы: {норма_мата}; процессуальные: {норма_проц});",
|
|
55
|
+
"расходы по уплате государственной пошлины возложить на Ответчика (ст. 110 АПК РФ)."
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"type": "attachments_list",
|
|
60
|
+
"items": [
|
|
61
|
+
"уведомление о вручении копии искового заявления Ответчику;",
|
|
62
|
+
"документ об уплате государственной пошлины (ст. 333.21 НК РФ);",
|
|
63
|
+
"документы, подтверждающие обстоятельства, на которых основаны требования;",
|
|
64
|
+
"копия свидетельства о государственной регистрации Истца;",
|
|
65
|
+
"документ, подтверждающий полномочия подписанта (доверенность / выписка из Устава) — ст. 61, 62 АПК РФ;",
|
|
66
|
+
"{приложение_6}"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "signature_block",
|
|
71
|
+
"left": "Дата: {дата}",
|
|
72
|
+
"right": "Представитель Истца _________________ /{фио_представителя}/"
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"profile": "court",
|
|
3
|
+
"output": {
|
|
4
|
+
"file_name": "{дд.мм.гггг}.Отзыв_на_исковое_заявление.docx",
|
|
5
|
+
"author": "{фио_представителя}"
|
|
6
|
+
},
|
|
7
|
+
"header": {
|
|
8
|
+
"align": "right",
|
|
9
|
+
"lines": [
|
|
10
|
+
"В {наименование_суда}",
|
|
11
|
+
"Судья: {фио_судьи}",
|
|
12
|
+
"Дело № {номер_дела}",
|
|
13
|
+
"Истец: {истец_наименование}, ИНН {инн_истца}, ОГРН {огрн_истца}",
|
|
14
|
+
"Ответчик: {ответчик_наименование}, ИНН {инн}, ОГРН {огрн}"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"title": {
|
|
18
|
+
"main": "ОТЗЫВ НА ИСКОВОЕ ЗАЯВЛЕНИЕ",
|
|
19
|
+
"subtitle": "(в порядке ст. 131 АПК РФ)"
|
|
20
|
+
},
|
|
21
|
+
"body": [
|
|
22
|
+
{
|
|
23
|
+
"type": "paragraph",
|
|
24
|
+
"text": "{ответчик_наименование} возражает против удовлетворения исковых требований {истец_наименование} по причинам:",
|
|
25
|
+
"inline_markdown": false
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "numbered_list",
|
|
29
|
+
"items": [
|
|
30
|
+
"{возражение_1}",
|
|
31
|
+
"{возражение_2}"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "paragraph",
|
|
36
|
+
"text": "Правовое обоснование возражений: {правовое_обоснование}. Доводы истца о том, что {довод_истца}, опровергаются следующим: {опровержение}.",
|
|
37
|
+
"inline_markdown": true
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"type": "paragraph",
|
|
41
|
+
"text": "Доказательства, подтверждающие возражения: {перечень_доказательств}.",
|
|
42
|
+
"inline_markdown": false
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"type": "paragraph",
|
|
46
|
+
"text": "На основании изложенного, руководствуясь ст. 131 АПК РФ, прошу:",
|
|
47
|
+
"inline_markdown": false
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "numbered_list",
|
|
51
|
+
"items": [
|
|
52
|
+
"в удовлетворении исковых требований {истец_наименование} к {ответчик_наименование} отказать полностью{или_частично}."
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"type": "attachments_list",
|
|
57
|
+
"items": [
|
|
58
|
+
"документ, подтверждающий направление копии отзыва истцу (ст. 131 АПК РФ);",
|
|
59
|
+
"документ о полномочиях представителя (доверенность) — ст. 61, 62 АПК РФ{приложение_3}",
|
|
60
|
+
"{приложение_4}"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"type": "signature_block",
|
|
65
|
+
"left": "Дата: {дата}",
|
|
66
|
+
"right": "Представитель Ответчика _________________ /{фио_представителя}/"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"profile": "soyu_rayon",
|
|
3
|
+
"output": {
|
|
4
|
+
"file_name": "{дд.мм.гггг}.Исковое_СОЮ_первой_инстанции.docx",
|
|
5
|
+
"author": "{фио_представителя}"
|
|
6
|
+
},
|
|
7
|
+
"header": {
|
|
8
|
+
"align": "right",
|
|
9
|
+
"lines": [
|
|
10
|
+
"В {наименование_районного_суда}",
|
|
11
|
+
"Для мирового судьи — участок: {наименование_судебного_участка}, мировой судья {фио_судьи}",
|
|
12
|
+
"Истец: {заявитель_наименование}, ИНН {инн}, ОГРН {огрн}",
|
|
13
|
+
"Адрес: {адрес_заявителя}",
|
|
14
|
+
"Представитель истца: {фио_представителя}",
|
|
15
|
+
"Ответчик: {ответчик_наименование}, ИНН {инн_ответчика}, ОГРН {огрн_ответчика}",
|
|
16
|
+
"Адрес: {адрес_ответчика}",
|
|
17
|
+
"Цена иска: {цена_иска} руб.",
|
|
18
|
+
"Госпошлина: {госпошлина} руб."
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"title": {
|
|
22
|
+
"main": "ИСКОВОЕ ЗАЯВЛЕНИЕ",
|
|
23
|
+
"subtitle": "{предмет_иска}"
|
|
24
|
+
},
|
|
25
|
+
"body": [
|
|
26
|
+
{
|
|
27
|
+
"type": "paragraph",
|
|
28
|
+
"text": "{обстоятельства_дела}",
|
|
29
|
+
"inline_markdown": true
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "paragraph",
|
|
33
|
+
"text": "Правовые основания требований: {правовые_основания} (вносятся legal-профилем через spec, шаблон их не содержит).",
|
|
34
|
+
"inline_markdown": true
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "paragraph",
|
|
38
|
+
"text": "На основании изложенного прошу:",
|
|
39
|
+
"inline_markdown": false
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "numbered_list",
|
|
43
|
+
"items": [
|
|
44
|
+
"{просительная_часть}"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"type": "attachments_list",
|
|
49
|
+
"items": [
|
|
50
|
+
"{приложение_1}",
|
|
51
|
+
"{приложение_2}",
|
|
52
|
+
"документ о полномочиях представителя — {реквизиты_доверенности}"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"type": "signature_block",
|
|
57
|
+
"left": "Дата: {дата}",
|
|
58
|
+
"right": "Представитель _________________ /{фио_представителя}/"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"profile": "vs_sud",
|
|
3
|
+
"output": {
|
|
4
|
+
"file_name": "{дд.мм.гггг}.Обращение_ВС.docx",
|
|
5
|
+
"author": "{фио_представителя}"
|
|
6
|
+
},
|
|
7
|
+
"header": {
|
|
8
|
+
"align": "right",
|
|
9
|
+
"lines": [
|
|
10
|
+
"В Верховный Суд Российской Федерации",
|
|
11
|
+
"Адрес суда: {адрес_суда}",
|
|
12
|
+
"Заявитель: {заявитель_наименование}, ИНН {инн}, ОГРН {огрн}",
|
|
13
|
+
"Представитель: {фио_представителя}, {репрезентативные_документы}",
|
|
14
|
+
"Дело № {номер_дела}",
|
|
15
|
+
"Суды, принявшие обжалуемые акты: {суды_по_делу}",
|
|
16
|
+
"Цена иска: {цена_иска} руб. (заполняется только для имущественных требований)",
|
|
17
|
+
"Госпошлина: {госпошлина} руб."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"title": {
|
|
21
|
+
"main": "ОБРАЩЕНИЕ",
|
|
22
|
+
"subtitle": "{вид_обращения}"
|
|
23
|
+
},
|
|
24
|
+
"body": [
|
|
25
|
+
{
|
|
26
|
+
"type": "paragraph",
|
|
27
|
+
"text": "Судебные акты по делу № {номер_дела}: {судебные_акты_по_делу}",
|
|
28
|
+
"inline_markdown": false
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "paragraph",
|
|
32
|
+
"text": "Обращение основано на следующем: {основания_обращения}",
|
|
33
|
+
"inline_markdown": true
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"type": "paragraph",
|
|
37
|
+
"text": "Правовые основания обращения: {правовые_основания} (вносятся legal-профилем через spec, шаблон их не содержит).",
|
|
38
|
+
"inline_markdown": true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"type": "paragraph",
|
|
42
|
+
"text": "На основании изложенного прошу:",
|
|
43
|
+
"inline_markdown": false
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"type": "numbered_list",
|
|
47
|
+
"items": [
|
|
48
|
+
"{просительная_часть}"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "attachments_list",
|
|
53
|
+
"items": [
|
|
54
|
+
"{приложение_1}",
|
|
55
|
+
"{приложение_2}",
|
|
56
|
+
"копии обжалуемых судебных актов — {количество_экземпляров}"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"type": "signature_block",
|
|
61
|
+
"left": "Дата: {дата}",
|
|
62
|
+
"right": "Представитель _________________ /{фио_представителя}/"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
docx_cli/agent_reader.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""docx-cli agent-read — ограниченное JSON-чтение docx для агентов.
|
|
2
|
+
|
|
3
|
+
v0.5.0 (clean-room: собственный agent-read для ИИ-агентов). Контракт: блоки
|
|
4
|
+
(параграф/таблица) с id, полными char-offset в конкатенированном тексте,
|
|
5
|
+
флагом complete и next_cursor — агент читает большие документы по окнам.
|
|
6
|
+
|
|
7
|
+
Fail-closed: отсутствующий файл -> ошибка-конверт.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from docx import Document
|
|
14
|
+
|
|
15
|
+
DEFAULT_MAX_CHARS = 4000
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AgentReadError(Exception):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _blocks(doc) -> list[dict]:
|
|
23
|
+
"""Блоки документа по порядку: параграфы и таблицы (строки->ячейки)."""
|
|
24
|
+
out: list[dict] = []
|
|
25
|
+
# python-docx не даёт единый поток тело->таблица по порядку просто; идём
|
|
26
|
+
# по XML тела в порядке следования.
|
|
27
|
+
from docx.table import Table
|
|
28
|
+
from docx.text.paragraph import Paragraph
|
|
29
|
+
body = doc.element.body
|
|
30
|
+
for child in body.iterchildren():
|
|
31
|
+
tag = child.tag.rsplit('}', 1)[-1]
|
|
32
|
+
if tag == 'p':
|
|
33
|
+
p = Paragraph(child, doc)
|
|
34
|
+
if p.text.strip():
|
|
35
|
+
out.append({"kind": "paragraph", "text": p.text})
|
|
36
|
+
elif tag == 'tbl':
|
|
37
|
+
t = Table(child, doc)
|
|
38
|
+
for row in t.rows:
|
|
39
|
+
cells = [c.text.strip() for c in row.cells if c.text.strip()]
|
|
40
|
+
if cells:
|
|
41
|
+
out.append({"kind": "table_row", "text": " | ".join(cells)})
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def read(path: str | Path, start_char: int = 0, max_chars: int = DEFAULT_MAX_CHARS) -> dict:
|
|
46
|
+
if isinstance(start_char, bool) or isinstance(max_chars, bool) or \
|
|
47
|
+
not isinstance(start_char, int) or not isinstance(max_chars, int):
|
|
48
|
+
raise AgentReadError("invalid_usage: max and cursor must be integers")
|
|
49
|
+
if start_char < 0 or max_chars <= 0:
|
|
50
|
+
raise AgentReadError("invalid_usage: max must be >0 and cursor must be >=0")
|
|
51
|
+
p = Path(path)
|
|
52
|
+
if not p.is_file():
|
|
53
|
+
raise AgentReadError(f"file not found: {p}")
|
|
54
|
+
doc = Document(str(p))
|
|
55
|
+
blocks = _blocks(doc)
|
|
56
|
+
# полный текст = конкатенация с \n; каждому блоку — диапазон offsets
|
|
57
|
+
enriched = []
|
|
58
|
+
cursor = 0
|
|
59
|
+
for i, b in enumerate(blocks):
|
|
60
|
+
start = cursor
|
|
61
|
+
text = b["text"]
|
|
62
|
+
cursor += len(text) + 1 # +1 за \n
|
|
63
|
+
enriched.append({"id": f"b{i}", "kind": b["kind"], "text": text,
|
|
64
|
+
"char_start": start, "char_end": cursor - 1})
|
|
65
|
+
total = cursor
|
|
66
|
+
if start_char > total or (total > 0 and start_char >= total):
|
|
67
|
+
raise AgentReadError(
|
|
68
|
+
f"invalid_usage: cursor {start_char} is beyond end (total_chars={total}); "
|
|
69
|
+
"use a cursor returned by a previous call or 0")
|
|
70
|
+
window_start = start_char
|
|
71
|
+
window_end = min(window_start + max_chars, total)
|
|
72
|
+
|
|
73
|
+
result = []
|
|
74
|
+
for b in enriched:
|
|
75
|
+
if b["char_end"] <= window_start or b["char_start"] >= window_end:
|
|
76
|
+
continue
|
|
77
|
+
text = b["text"]
|
|
78
|
+
s_off = max(window_start - b["char_start"], 0)
|
|
79
|
+
e_off = min(window_end - b["char_start"], len(text))
|
|
80
|
+
frag = text[s_off:e_off]
|
|
81
|
+
result.append({"id": b["id"], "kind": b["kind"], "text": frag,
|
|
82
|
+
"char_start": b["char_start"] + s_off,
|
|
83
|
+
"complete": s_off == 0 and e_off == len(text)})
|
|
84
|
+
|
|
85
|
+
next_cursor = window_end if window_end < total else None
|
|
86
|
+
return {"total_chars": total, "window": [window_start, window_end],
|
|
87
|
+
"blocks": result, "next_cursor": next_cursor}
|