gost-docx 0.1.0__tar.gz
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.
- gost_docx-0.1.0/PKG-INFO +11 -0
- gost_docx-0.1.0/README.md +0 -0
- gost_docx-0.1.0/pyproject.toml +20 -0
- gost_docx-0.1.0/src/gost/__init__.py +3 -0
- gost_docx-0.1.0/src/gost/element_factory.py +24 -0
- gost_docx-0.1.0/src/gost/elements/__init__.py +0 -0
- gost_docx-0.1.0/src/gost/elements/element.py +15 -0
- gost_docx-0.1.0/src/gost/elements/head.py +32 -0
- gost_docx-0.1.0/src/gost/elements/image.py +39 -0
- gost_docx-0.1.0/src/gost/elements/table.py +93 -0
- gost_docx-0.1.0/src/gost/elements/text.py +13 -0
- gost_docx-0.1.0/src/gost/index/__init__.py +0 -0
- gost_docx-0.1.0/src/gost/index/counter.py +45 -0
- gost_docx-0.1.0/src/gost/index/index_manager.py +48 -0
- gost_docx-0.1.0/src/gost/index/index_type.py +11 -0
- gost_docx-0.1.0/src/gost/styles.py +81 -0
- gost_docx-0.1.0/src/gost/utils.py +21 -0
- gost_docx-0.1.0/src/gost/word_builder.py +39 -0
gost_docx-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: gost-docx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Dmitry Klimov
|
|
6
|
+
Author-email: Dmitry Klimov <dmitry.v.klimov@gmail.com>
|
|
7
|
+
Requires-Dist: pandas>=2.3.2,<3.0.0
|
|
8
|
+
Requires-Dist: python-docx>=1.2.0
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "gost-docx"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Dmitry Klimov", email = "dmitry.v.klimov@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pandas>=2.3.2,<3.0.0",
|
|
12
|
+
"python-docx>=1.2.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["uv_build>=0.10.8,<0.11.0"]
|
|
17
|
+
build-backend = "uv_build"
|
|
18
|
+
|
|
19
|
+
[tool.uv.build-backend]
|
|
20
|
+
module-name = "gost"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from gost.index.head_counter import HeadCounter
|
|
2
|
+
from gost.index.counter import Counter
|
|
3
|
+
from gost.elements.head import Head
|
|
4
|
+
from gost.elements.image import Image
|
|
5
|
+
from gost.elements.text import Text
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ElementFactory:
|
|
9
|
+
def __init__(self, character: str) -> None:
|
|
10
|
+
self.__image_counter = Counter()
|
|
11
|
+
self.__table_counter = Counter()
|
|
12
|
+
self.__head_counter = HeadCounter()
|
|
13
|
+
self.__character = character
|
|
14
|
+
|
|
15
|
+
def create_text(self, text: str) -> Text:
|
|
16
|
+
return Text(text)
|
|
17
|
+
|
|
18
|
+
def create_head(self, use_numbers: bool, text: str, level: int = 1) -> Head:
|
|
19
|
+
number = self.__head_counter.get_next(level)
|
|
20
|
+
return Head(number, use_numbers, text, level)
|
|
21
|
+
|
|
22
|
+
def create_image(self, path: str, alt: str) -> Image:
|
|
23
|
+
number = self.__image_counter.get_next()
|
|
24
|
+
return Image(number, alt, path)
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from docx.document import Document
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ElementBase(ABC):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def render(self, document: Document) -> None:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NumberedElement(ElementBase, ABC):
|
|
14
|
+
def __init__(self, number: str) -> None:
|
|
15
|
+
self.number = number
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from docx.document import Document
|
|
2
|
+
|
|
3
|
+
from gost.elements.element import NumberedElement
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Head(NumberedElement):
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
number: str,
|
|
10
|
+
use_number: bool,
|
|
11
|
+
text: str,
|
|
12
|
+
level: int = 1,
|
|
13
|
+
) -> None:
|
|
14
|
+
super().__init__(number)
|
|
15
|
+
self.text = text
|
|
16
|
+
self.level = level
|
|
17
|
+
self.use_number = use_number
|
|
18
|
+
|
|
19
|
+
def render(self, document: Document) -> None:
|
|
20
|
+
text = self.__prepare_render_text()
|
|
21
|
+
document.add_heading(text, level=self.level)
|
|
22
|
+
|
|
23
|
+
def __prepare_render_text(self) -> str:
|
|
24
|
+
if self.use_number:
|
|
25
|
+
text = f"{self.use_number} {self.text}"
|
|
26
|
+
else:
|
|
27
|
+
text = f"{self.text}"
|
|
28
|
+
|
|
29
|
+
if self.level == 1:
|
|
30
|
+
text = self.text.upper()
|
|
31
|
+
|
|
32
|
+
return text
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from docx.document import Document
|
|
4
|
+
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
|
5
|
+
from docx.shared import Cm, Pt
|
|
6
|
+
|
|
7
|
+
from gost.elements.element import NumberedElement
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Image(NumberedElement):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
number: str,
|
|
14
|
+
path: str,
|
|
15
|
+
alt: str,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Создает изображение с подписью «Рисунок number — alt».
|
|
19
|
+
"""
|
|
20
|
+
super().__init__(number)
|
|
21
|
+
self.alt = alt
|
|
22
|
+
self.path = Path(path)
|
|
23
|
+
|
|
24
|
+
def render(self, document: Document) -> None:
|
|
25
|
+
if self.path.exists():
|
|
26
|
+
document.add_picture(str(self.path), width=Cm(16.5))
|
|
27
|
+
last_paragraph = document.paragraphs[-1]
|
|
28
|
+
last_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
29
|
+
last_paragraph.paragraph_format.first_line_indent = Cm(0)
|
|
30
|
+
else:
|
|
31
|
+
document.add_paragraph(f"[Изображение не найдено: {self.path}]")
|
|
32
|
+
|
|
33
|
+
caption = document.add_paragraph()
|
|
34
|
+
caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
35
|
+
caption.paragraph_format.first_line_indent = Cm(0)
|
|
36
|
+
caption.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
|
37
|
+
caption.paragraph_format.space_after = Pt(6)
|
|
38
|
+
run = caption.add_run(f"Рисунок {self.number} – {self.alt}")
|
|
39
|
+
run.font.size = Pt(14)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from docx.document import Document
|
|
5
|
+
from docx.enum.table import WD_TABLE_ALIGNMENT
|
|
6
|
+
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
|
7
|
+
from docx.shared import Cm, Pt
|
|
8
|
+
|
|
9
|
+
from gost.elements.element import NumberedElement
|
|
10
|
+
from gost.utils import format_value
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Table(NumberedElement):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
number: str,
|
|
17
|
+
df: pd.DataFrame,
|
|
18
|
+
float_format: Optional[str] = ".1f",
|
|
19
|
+
title: Optional[str] = None,
|
|
20
|
+
show_header: bool = True,
|
|
21
|
+
show_index: bool = True,
|
|
22
|
+
bold_header: bool = False,
|
|
23
|
+
bold_index: bool = False,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Создает таблицу из DataFrame с подписью «Таблица number — title».
|
|
26
|
+
|
|
27
|
+
Подпись добавляется только при наличии title, при этом счётчик таблиц увеличивается.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
df: Данные для отображения в таблице.
|
|
31
|
+
float_format: Формат вещественных чисел (например, «.1f»). None — без форматирования.
|
|
32
|
+
title: Заголовок таблицы. Если None, подпись и счётчик не добавляются.
|
|
33
|
+
show_header: Добавлять ли строку с именами столбцов.
|
|
34
|
+
show_index: Добавлять ли столбец с индексом DataFrame.
|
|
35
|
+
bold_header: Выделять ли заголовки столбцов жирным.
|
|
36
|
+
bold_index: Выделять ли значения индекса жирным.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Текущий экземпляр WordBuilder для цепочки вызовов.
|
|
40
|
+
"""
|
|
41
|
+
super().__init__(number)
|
|
42
|
+
self.df = df
|
|
43
|
+
self.float_format = float_format
|
|
44
|
+
self.title = title
|
|
45
|
+
self.show_header = show_header
|
|
46
|
+
self.show_index = show_index
|
|
47
|
+
self.bold_header = bold_header
|
|
48
|
+
self.bold_index = bold_index
|
|
49
|
+
|
|
50
|
+
def render(self, document: Document) -> None:
|
|
51
|
+
if self.title is not None:
|
|
52
|
+
caption = document.add_paragraph()
|
|
53
|
+
caption.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
|
54
|
+
caption.paragraph_format.first_line_indent = Cm(0)
|
|
55
|
+
caption.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
|
56
|
+
caption.paragraph_format.space_after = Pt(6)
|
|
57
|
+
run = caption.add_run(f"Таблица – {self.number} {self.title}")
|
|
58
|
+
run.font.size = Pt(14)
|
|
59
|
+
|
|
60
|
+
header_rows = 1 if self.show_header else 0
|
|
61
|
+
rows = len(self.df) + header_rows
|
|
62
|
+
index_offset = 1 if self.show_index else 0
|
|
63
|
+
cols = len(self.df.columns) + index_offset
|
|
64
|
+
|
|
65
|
+
table = document.add_table(rows=rows, cols=cols)
|
|
66
|
+
table.style = "Table Grid"
|
|
67
|
+
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
|
68
|
+
|
|
69
|
+
if self.show_header:
|
|
70
|
+
if self.show_index:
|
|
71
|
+
table.cell(0, 0).text = ""
|
|
72
|
+
for j, col_name in enumerate(self.df.columns):
|
|
73
|
+
cell = table.cell(0, j + index_offset)
|
|
74
|
+
cell.text = ""
|
|
75
|
+
run = cell.paragraphs[0].add_run(format_value(col_name, self.float_format))
|
|
76
|
+
run.bold = self.bold_header
|
|
77
|
+
|
|
78
|
+
for i, (idx, row) in enumerate(self.df.iterrows()):
|
|
79
|
+
row_idx = i + header_rows
|
|
80
|
+
if self.show_index:
|
|
81
|
+
idx_cell = table.cell(row_idx, 0)
|
|
82
|
+
idx_cell.text = ""
|
|
83
|
+
run = idx_cell.paragraphs[0].add_run(str(idx))
|
|
84
|
+
run.bold = self.bold_index
|
|
85
|
+
for j, val in enumerate(row):
|
|
86
|
+
table.cell(row_idx, j + index_offset).text = format_value(val, self.float_format)
|
|
87
|
+
|
|
88
|
+
for row in table.rows:
|
|
89
|
+
for cell in row.cells:
|
|
90
|
+
for para in cell.paragraphs:
|
|
91
|
+
para.style = "Table Text"
|
|
92
|
+
|
|
93
|
+
document.add_paragraph()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from docx.document import Document
|
|
2
|
+
|
|
3
|
+
from gost.elements.element import ElementBase
|
|
4
|
+
from gost.utils import render_bold
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Text(ElementBase):
|
|
8
|
+
def __init__(self, text: str) -> None:
|
|
9
|
+
self.text = text
|
|
10
|
+
|
|
11
|
+
def render(self, document: Document) -> None:
|
|
12
|
+
paragraph = document.add_paragraph()
|
|
13
|
+
render_bold(paragraph, self.text)
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class Counter:
|
|
2
|
+
def __init__(self, initial: int = 1) -> None:
|
|
3
|
+
self.__initial = initial - 1
|
|
4
|
+
self.current: int = self.__initial
|
|
5
|
+
|
|
6
|
+
def get_next(self) -> int:
|
|
7
|
+
self.current = self.current + 1
|
|
8
|
+
return self.current
|
|
9
|
+
|
|
10
|
+
def reset(self):
|
|
11
|
+
self.current = self.__initial
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HeadCounter:
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
self.__levels_counter: dict[int, Counter] = {
|
|
17
|
+
1: Counter(),
|
|
18
|
+
2: Counter(),
|
|
19
|
+
3: Counter(),
|
|
20
|
+
4: Counter(),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def get_next(self, head_level: int) -> str:
|
|
24
|
+
self.__levels_counter[head_level].get_next()
|
|
25
|
+
|
|
26
|
+
result_number = str(self.__levels_counter[1].current)
|
|
27
|
+
if head_level > 1:
|
|
28
|
+
for level in range(2, head_level + 1):
|
|
29
|
+
result_number = result_number + '.' + str(self.__levels_counter[level].current)
|
|
30
|
+
|
|
31
|
+
return result_number
|
|
32
|
+
|
|
33
|
+
def get_current_chapter(self) -> int:
|
|
34
|
+
return self.__levels_counter[1].current
|
|
35
|
+
|
|
36
|
+
def reset(self):
|
|
37
|
+
for level in self.__levels_counter:
|
|
38
|
+
self.__levels_counter[level].reset()
|
|
39
|
+
|
|
40
|
+
def __get_currents(self) -> list[int]:
|
|
41
|
+
currents: list[int] = []
|
|
42
|
+
for level in self.__levels_counter:
|
|
43
|
+
currents.append(self.__levels_counter[level].current)
|
|
44
|
+
|
|
45
|
+
return currents
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from gost.index.counter import Counter, HeadCounter
|
|
2
|
+
from gost.index.index_type import IndexType
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class IndexManager:
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
character: str | None = None,
|
|
9
|
+
index_type: IndexType = IndexType.CONTINUOUS
|
|
10
|
+
) -> None:
|
|
11
|
+
self.__character = character
|
|
12
|
+
self.__index_type = index_type
|
|
13
|
+
|
|
14
|
+
self.__head_counter = HeadCounter()
|
|
15
|
+
self.__image_counter = Counter()
|
|
16
|
+
self.__table_counter = Counter()
|
|
17
|
+
|
|
18
|
+
def get_head_index(self, level: int) -> str:
|
|
19
|
+
index = self.__head_counter.get_next(level)
|
|
20
|
+
|
|
21
|
+
if self.__character:
|
|
22
|
+
index = self.__add_character(index)
|
|
23
|
+
|
|
24
|
+
if level == 0 and self.__index_type == IndexType.CHAPTER_RELATIVE:
|
|
25
|
+
self.__reset_counters()
|
|
26
|
+
return index
|
|
27
|
+
|
|
28
|
+
def get_image_index(self):
|
|
29
|
+
index = str(self.__image_counter.get_next())
|
|
30
|
+
if self.__index_type == IndexType.CHAPTER_RELATIVE:
|
|
31
|
+
index = self.__add_chapter(index)
|
|
32
|
+
|
|
33
|
+
if self.__character:
|
|
34
|
+
index = self.__add_character(index)
|
|
35
|
+
|
|
36
|
+
return index
|
|
37
|
+
|
|
38
|
+
def __add_chapter(self, index: str) -> str:
|
|
39
|
+
current_chapter = self.__head_counter.get_current_chapter()
|
|
40
|
+
index = f"{current_chapter}.{index}"
|
|
41
|
+
return index
|
|
42
|
+
|
|
43
|
+
def __add_character(self, index: str) -> str:
|
|
44
|
+
return f"{self.__character}.{index}"
|
|
45
|
+
|
|
46
|
+
def __reset_counters(self):
|
|
47
|
+
self.__image_counter.reset()
|
|
48
|
+
self.__head_counter.reset()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class IndexType(Enum):
|
|
5
|
+
"""Режим нумерации объектов документа (рисунков, таблиц, формул)."""
|
|
6
|
+
|
|
7
|
+
CONTINUOUS = auto()
|
|
8
|
+
"""Сквозная нумерация по всему документу: 1, 2, 3, ..."""
|
|
9
|
+
|
|
10
|
+
CHAPTER_RELATIVE = auto()
|
|
11
|
+
"""Нумерация относительно номера текущей главы: 1.1, 1.2, 2.1, ..."""
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from docx.document import Document
|
|
2
|
+
from docx.enum.style import WD_STYLE_TYPE
|
|
3
|
+
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
|
4
|
+
from docx.oxml.ns import qn
|
|
5
|
+
from docx.shared import Cm, Pt, RGBColor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def apply_gost_styles(doc: Document) -> None:
|
|
9
|
+
"""Configure GOST 7.32-2001 paragraph and heading styles on *doc* in-place."""
|
|
10
|
+
_apply_section(doc)
|
|
11
|
+
_apply_normal_style(doc)
|
|
12
|
+
_apply_table_text_style(doc)
|
|
13
|
+
_apply_heading_styles(doc)
|
|
14
|
+
|
|
15
|
+
def _apply_section(doc: Document) -> None:
|
|
16
|
+
# Page margins per ГОСТ 7.32-2001: left ≥ 30mm, right ≥ 10mm, top ≥ 20mm, bottom ≥ 20mm
|
|
17
|
+
section = doc.sections[0]
|
|
18
|
+
section.left_margin = Cm(3.0)
|
|
19
|
+
section.right_margin = Cm(1.5)
|
|
20
|
+
section.top_margin = Cm(2.0)
|
|
21
|
+
section.bottom_margin = Cm(2.0)
|
|
22
|
+
|
|
23
|
+
def _apply_normal_style(doc: Document) -> None:
|
|
24
|
+
normal = doc.styles["Normal"]
|
|
25
|
+
normal.font.name = "Times New Roman"
|
|
26
|
+
normal.font.size = Pt(14)
|
|
27
|
+
normal.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
|
28
|
+
normal.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
|
29
|
+
normal.paragraph_format.first_line_indent = Cm(1.25)
|
|
30
|
+
normal.paragraph_format.space_before = Pt(0)
|
|
31
|
+
normal.paragraph_format.space_after = Pt(0)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _apply_table_text_style(doc: Document) -> None:
|
|
35
|
+
table_text = doc.styles.add_style("Table Text", WD_STYLE_TYPE.PARAGRAPH)
|
|
36
|
+
table_text.font.name = "Times New Roman"
|
|
37
|
+
table_text.font.size = Pt(12)
|
|
38
|
+
table_text.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
|
39
|
+
table_text.paragraph_format.first_line_indent = Cm(0)
|
|
40
|
+
table_text.paragraph_format.space_before = Pt(0)
|
|
41
|
+
table_text.paragraph_format.space_after = Pt(0)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _apply_heading_styles(doc: Document) -> None:
|
|
45
|
+
# (style_name, centered, font_size, space_before)
|
|
46
|
+
heading_configs = [
|
|
47
|
+
("Heading 1", True, Pt(14), Pt(0)),
|
|
48
|
+
("Heading 2", False, Pt(14), Pt(0)),
|
|
49
|
+
("Heading 3", False, Pt(14), Pt(0)),
|
|
50
|
+
("Heading 4", False, Pt(14), Pt(0)),
|
|
51
|
+
]
|
|
52
|
+
for style_name, centered, font_size, space_before in heading_configs:
|
|
53
|
+
s = doc.styles[style_name]
|
|
54
|
+
s.font.name = "Times New Roman"
|
|
55
|
+
_remove_theme_font_overrides(s)
|
|
56
|
+
s.font.size = font_size
|
|
57
|
+
s.font.bold = True
|
|
58
|
+
s.font.italic = False
|
|
59
|
+
s.font.color.rgb = RGBColor(0, 0, 0)
|
|
60
|
+
s.paragraph_format.alignment = (
|
|
61
|
+
WD_ALIGN_PARAGRAPH.CENTER if centered else WD_ALIGN_PARAGRAPH.LEFT
|
|
62
|
+
)
|
|
63
|
+
s.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
|
64
|
+
s.paragraph_format.first_line_indent = Cm(0) if centered else Cm(1.25)
|
|
65
|
+
s.paragraph_format.space_before = space_before
|
|
66
|
+
s.paragraph_format.space_after = Pt(0)
|
|
67
|
+
_remove_bottom_border(s)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _remove_theme_font_overrides(style) -> None:
|
|
71
|
+
rPr = style.element.get_or_add_rPr()
|
|
72
|
+
rFonts = rPr.find(qn("w:rFonts"))
|
|
73
|
+
if rFonts is not None:
|
|
74
|
+
for attr in [qn("w:asciiTheme"), qn("w:hAnsiTheme"), qn("w:eastAsiaTheme"), qn("w:cstheme")]:
|
|
75
|
+
rFonts.attrib.pop(attr, None)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _remove_bottom_border(style) -> None:
|
|
79
|
+
pPr = style.element.get_or_add_pPr()
|
|
80
|
+
for pBdr in pPr.findall(qn("w:pBdr")):
|
|
81
|
+
pPr.remove(pBdr)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def format_value(val, float_format: Optional[str]) -> str:
|
|
6
|
+
if isinstance(val, float):
|
|
7
|
+
if float_format:
|
|
8
|
+
return f"{val:{float_format}}"
|
|
9
|
+
return str(val)
|
|
10
|
+
return str(val)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def render_bold(paragraph, text: str) -> None:
|
|
14
|
+
"""Parse **bold** fragments into Word runs."""
|
|
15
|
+
parts = re.split(r"(\*\*[^*]+\*\*)", text)
|
|
16
|
+
for part in parts:
|
|
17
|
+
if part.startswith("**") and part.endswith("**"):
|
|
18
|
+
run = paragraph.add_run(part[2:-2])
|
|
19
|
+
run.bold = True
|
|
20
|
+
else:
|
|
21
|
+
paragraph.add_run(part)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from docx import Document
|
|
4
|
+
|
|
5
|
+
from .elements.element import ElementBase
|
|
6
|
+
from .styles import apply_gost_styles
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WordBuilder:
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Инициализирует документ с полями по ГОСТ 7.32-2001 и применяет стили.
|
|
13
|
+
"""
|
|
14
|
+
self.__document = Document()
|
|
15
|
+
self.__is_rendered = False
|
|
16
|
+
self.__figure_counter = 0
|
|
17
|
+
self.__table_counter = 0
|
|
18
|
+
self.__elements: list[ElementBase] = []
|
|
19
|
+
|
|
20
|
+
apply_gost_styles(self.__document)
|
|
21
|
+
|
|
22
|
+
def add_element(self, element: ElementBase) -> None:
|
|
23
|
+
self.__elements.append(element)
|
|
24
|
+
|
|
25
|
+
def save(self, path: Path) -> None:
|
|
26
|
+
"""Сохраняет документ в файл формата .docx.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
path: Путь к итоговому файлу.
|
|
30
|
+
"""
|
|
31
|
+
if not self.__is_rendered:
|
|
32
|
+
self.__render_elements()
|
|
33
|
+
self.__is_rendered = True
|
|
34
|
+
|
|
35
|
+
self.__document.save(str(path))
|
|
36
|
+
|
|
37
|
+
def __render_elements(self):
|
|
38
|
+
for element in self.__elements:
|
|
39
|
+
element.render(self.__document)
|