chgksuite 0.26.1__py3-none-any.whl → 0.27.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.
- chgksuite/_html2md.py +90 -0
- chgksuite/cli.py +21 -8
- chgksuite/common.py +16 -12
- chgksuite/composer/__init__.py +9 -7
- chgksuite/composer/chgksuite_parser.py +16 -7
- chgksuite/composer/composer_common.py +14 -5
- chgksuite/composer/db.py +1 -2
- chgksuite/composer/docx.py +29 -8
- chgksuite/composer/latex.py +1 -2
- chgksuite/composer/lj.py +1 -2
- chgksuite/composer/{reddit.py → markdown.py} +35 -25
- chgksuite/composer/openquiz.py +2 -3
- chgksuite/composer/pptx.py +2 -2
- chgksuite/composer/telegram.py +2 -1
- chgksuite/handouter/gen.py +11 -7
- chgksuite/handouter/installer.py +0 -0
- chgksuite/handouter/runner.py +234 -10
- chgksuite/handouter/tex_internals.py +12 -13
- chgksuite/lastdir +1 -0
- chgksuite/parser.py +32 -33
- chgksuite/parser_db.py +4 -6
- chgksuite/resources/labels_az.toml +22 -0
- chgksuite/resources/labels_by.toml +1 -2
- chgksuite/resources/labels_by_tar.toml +1 -2
- chgksuite/resources/labels_en.toml +1 -2
- chgksuite/resources/labels_kz_cyr.toml +1 -2
- chgksuite/resources/labels_ru.toml +1 -2
- chgksuite/resources/labels_sr.toml +1 -2
- chgksuite/resources/labels_ua.toml +1 -2
- chgksuite/resources/labels_uz.toml +0 -3
- chgksuite/resources/labels_uz_cyr.toml +1 -2
- chgksuite/resources/regexes_az.json +17 -0
- chgksuite/resources/regexes_by.json +3 -2
- chgksuite/resources/regexes_by_tar.json +17 -0
- chgksuite/resources/regexes_en.json +3 -2
- chgksuite/resources/regexes_kz_cyr.json +3 -2
- chgksuite/resources/regexes_ru.json +3 -2
- chgksuite/resources/regexes_sr.json +3 -2
- chgksuite/resources/regexes_ua.json +3 -2
- chgksuite/resources/regexes_uz.json +16 -0
- chgksuite/resources/regexes_uz_cyr.json +3 -2
- chgksuite/trello.py +8 -9
- chgksuite/typotools.py +9 -8
- chgksuite/version.py +1 -1
- {chgksuite-0.26.1.dist-info → chgksuite-0.27.0.dist-info}/METADATA +10 -19
- chgksuite-0.27.0.dist-info/RECORD +63 -0
- {chgksuite-0.26.1.dist-info → chgksuite-0.27.0.dist-info}/WHEEL +1 -2
- chgksuite/composer/telegram_parser.py +0 -230
- chgksuite-0.26.1.dist-info/RECORD +0 -59
- chgksuite-0.26.1.dist-info/top_level.txt +0 -1
- {chgksuite-0.26.1.dist-info → chgksuite-0.27.0.dist-info}/entry_points.txt +0 -0
- {chgksuite-0.26.1.dist-info → chgksuite-0.27.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import codecs
|
|
2
1
|
import os
|
|
3
2
|
|
|
4
3
|
from chgksuite.composer.composer_common import (
|
|
@@ -9,20 +8,20 @@ from chgksuite.composer.composer_common import (
|
|
|
9
8
|
)
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
class
|
|
11
|
+
class MarkdownExporter(BaseExporter):
|
|
13
12
|
def __init__(self, *args, **kwargs):
|
|
14
13
|
super().__init__(*args, **kwargs)
|
|
15
14
|
self.im = Imgur(self.args.imgur_client_id or IMGUR_CLIENT_ID)
|
|
16
15
|
self.qcount = 1
|
|
17
16
|
|
|
18
|
-
def
|
|
17
|
+
def markdownyapper(self, e):
|
|
19
18
|
if isinstance(e, str):
|
|
20
|
-
return self.
|
|
19
|
+
return self.markdown_element_layout(e)
|
|
21
20
|
elif isinstance(e, list):
|
|
22
21
|
if not any(isinstance(x, list) for x in e):
|
|
23
|
-
return self.
|
|
22
|
+
return self.markdown_element_layout(e)
|
|
24
23
|
else:
|
|
25
|
-
return " \n".join([self.
|
|
24
|
+
return " \n".join([self.markdown_element_layout(x) for x in e])
|
|
26
25
|
|
|
27
26
|
def parse_and_upload_image(self, path):
|
|
28
27
|
parsed_image = parseimg(
|
|
@@ -37,11 +36,13 @@ class RedditExporter(BaseExporter):
|
|
|
37
36
|
imglink = uploaded_image["data"]["link"]
|
|
38
37
|
return imglink
|
|
39
38
|
|
|
40
|
-
def
|
|
39
|
+
def markdownformat(self, s):
|
|
41
40
|
res = ""
|
|
42
41
|
for run in self.parse_4s_elem(s):
|
|
43
|
-
if run[0]
|
|
42
|
+
if run[0] == "":
|
|
44
43
|
res += run[1]
|
|
44
|
+
if run[0] == "hyperlink":
|
|
45
|
+
res += "<{}>".format(run[1])
|
|
45
46
|
if run[0] == "screen":
|
|
46
47
|
res += run[1]["for_screen"]
|
|
47
48
|
if run[0] == "italic":
|
|
@@ -51,61 +52,70 @@ class RedditExporter(BaseExporter):
|
|
|
51
52
|
imglink = run[1]
|
|
52
53
|
else:
|
|
53
54
|
imglink = self.parse_and_upload_image(run[1])
|
|
54
|
-
|
|
55
|
+
if self.args.filetype == "redditmd":
|
|
56
|
+
res += "[картинка]({})".format(imglink)
|
|
57
|
+
else:
|
|
58
|
+
res += "".format(imglink)
|
|
55
59
|
while res.endswith("\n"):
|
|
56
60
|
res = res[:-1]
|
|
57
61
|
res = res.replace("\n", " \n")
|
|
58
62
|
return res
|
|
59
63
|
|
|
60
|
-
def
|
|
64
|
+
def markdown_element_layout(self, e):
|
|
61
65
|
res = ""
|
|
62
66
|
if isinstance(e, str):
|
|
63
|
-
res = self.
|
|
67
|
+
res = self.markdownformat(e)
|
|
64
68
|
return res
|
|
65
69
|
if isinstance(e, list):
|
|
66
70
|
res = " \n".join(
|
|
67
71
|
[
|
|
68
|
-
"{}\\. {}".format(i + 1, self.
|
|
72
|
+
"{}\\. {}".format(i + 1, self.markdown_element_layout(x))
|
|
69
73
|
for i, x in enumerate(e)
|
|
70
74
|
]
|
|
71
75
|
)
|
|
72
76
|
return res
|
|
73
77
|
|
|
74
|
-
def
|
|
78
|
+
def markdown_format_element(self, pair):
|
|
75
79
|
if pair[0] == "Question":
|
|
76
|
-
return self.
|
|
80
|
+
return self.markdown_format_question(pair[1])
|
|
77
81
|
|
|
78
|
-
def
|
|
82
|
+
def markdown_format_question(self, q):
|
|
79
83
|
if "setcounter" in q:
|
|
80
84
|
self.qcount = int(q["setcounter"])
|
|
81
85
|
res = "__Вопрос {}__: {} \n".format(
|
|
82
86
|
self.qcount if "number" not in q else q["number"],
|
|
83
|
-
self.
|
|
87
|
+
self.markdownyapper(q["question"]),
|
|
84
88
|
)
|
|
85
89
|
if "number" not in q:
|
|
86
90
|
self.qcount += 1
|
|
87
|
-
|
|
91
|
+
spoiler_start = ">!" if self.args.filetype == "redditmd" else ""
|
|
92
|
+
spoiler_end = "!<" if self.args.filetype == "redditmd" else ""
|
|
93
|
+
res += "__Ответ:__ {}{} \n".format(
|
|
94
|
+
spoiler_start, self.markdownyapper(q["answer"])
|
|
95
|
+
)
|
|
88
96
|
if "zachet" in q:
|
|
89
|
-
res += "__Зачёт:__ {} \n".format(self.
|
|
97
|
+
res += "__Зачёт:__ {} \n".format(self.markdownyapper(q["zachet"]))
|
|
90
98
|
if "nezachet" in q:
|
|
91
|
-
res += "__Незачёт:__ {} \n".format(self.
|
|
99
|
+
res += "__Незачёт:__ {} \n".format(self.markdownyapper(q["nezachet"]))
|
|
92
100
|
if "comment" in q:
|
|
93
|
-
res += "__Комментарий:__ {} \n".format(self.
|
|
101
|
+
res += "__Комментарий:__ {} \n".format(self.markdownyapper(q["comment"]))
|
|
94
102
|
if "source" in q:
|
|
95
|
-
res += "__Источник:__ {} \n".format(self.
|
|
103
|
+
res += "__Источник:__ {} \n".format(self.markdownyapper(q["source"]))
|
|
96
104
|
if "author" in q:
|
|
97
|
-
res += "
|
|
105
|
+
res += "{}\n__Автор:__ {} \n".format(
|
|
106
|
+
spoiler_end, self.markdownyapper(q["author"])
|
|
107
|
+
)
|
|
98
108
|
else:
|
|
99
|
-
res += "
|
|
109
|
+
res += spoiler_end + "\n"
|
|
100
110
|
return res
|
|
101
111
|
|
|
102
112
|
def export(self, outfile):
|
|
103
113
|
result = []
|
|
104
114
|
for pair in self.structure:
|
|
105
|
-
res = self.
|
|
115
|
+
res = self.markdown_format_element(pair)
|
|
106
116
|
if res:
|
|
107
117
|
result.append(res)
|
|
108
118
|
text = "\n\n".join(result)
|
|
109
|
-
with
|
|
119
|
+
with open(outfile, "w", encoding="utf-8") as f:
|
|
110
120
|
f.write(text)
|
|
111
121
|
self.logger.info("Output: {}".format(outfile))
|
chgksuite/composer/openquiz.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import codecs
|
|
2
1
|
import copy
|
|
3
2
|
import re
|
|
4
3
|
import json
|
|
@@ -71,7 +70,7 @@ class OpenquizExporter(BaseExporter):
|
|
|
71
70
|
)
|
|
72
71
|
while res.endswith("\n"):
|
|
73
72
|
res = res[:-1]
|
|
74
|
-
hs = self.
|
|
73
|
+
hs = self.regexes["handout_short"]
|
|
75
74
|
if images:
|
|
76
75
|
res = re.sub("\\[" + hs + "(.+?)\\]", "", s, flags=re.DOTALL)
|
|
77
76
|
res = res.strip()
|
|
@@ -175,5 +174,5 @@ class OpenquizExporter(BaseExporter):
|
|
|
175
174
|
result = []
|
|
176
175
|
for q in questions:
|
|
177
176
|
result.append(self.oq_format_question(q))
|
|
178
|
-
with
|
|
177
|
+
with open(outfilename, "w", encoding="utf-8") as f:
|
|
179
178
|
f.write(json.dumps(result, indent=2, ensure_ascii=False))
|
chgksuite/composer/pptx.py
CHANGED
|
@@ -121,13 +121,13 @@ class PptxExporter(BaseExporter):
|
|
|
121
121
|
replace_spaces=True,
|
|
122
122
|
do_not_remove_accents=False,
|
|
123
123
|
):
|
|
124
|
-
hs = self.
|
|
124
|
+
hs = self.regexes["handout_short"]
|
|
125
125
|
if isinstance(s, list):
|
|
126
126
|
for i in range(len(s)):
|
|
127
127
|
s[i] = self.pptx_process_text(s[i], image=image)
|
|
128
128
|
return s
|
|
129
129
|
if not (self.args.do_not_remove_accents or do_not_remove_accents):
|
|
130
|
-
s = remove_accents_standalone(s, self.
|
|
130
|
+
s = remove_accents_standalone(s, self.regexes)
|
|
131
131
|
if strip_brackets:
|
|
132
132
|
s = self.remove_square_brackets(s)
|
|
133
133
|
s = s.replace("]\n", "]\n\n")
|
chgksuite/composer/telegram.py
CHANGED
|
@@ -6,6 +6,7 @@ import sqlite3
|
|
|
6
6
|
import tempfile
|
|
7
7
|
import time
|
|
8
8
|
import uuid
|
|
9
|
+
from typing import Optional, Union
|
|
9
10
|
|
|
10
11
|
import requests
|
|
11
12
|
import toml
|
|
@@ -247,7 +248,7 @@ class TelegramExporter(BaseExporter):
|
|
|
247
248
|
channel_id_str = channel_id_str[4:]
|
|
248
249
|
return f"https://t.me/c/{channel_id_str}/{message_id}"
|
|
249
250
|
|
|
250
|
-
def extract_id_from_link(self, link) -> int
|
|
251
|
+
def extract_id_from_link(self, link) -> Optional[Union[int, str]]:
|
|
251
252
|
"""
|
|
252
253
|
Extract channel or chat ID from a Telegram link.
|
|
253
254
|
Examples:
|
chgksuite/handouter/gen.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
import itertools
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
6
7
|
from collections import defaultdict
|
|
@@ -53,13 +54,16 @@ def generate_handouts_list(handouts, output_dir, base_name, parsed):
|
|
|
53
54
|
|
|
54
55
|
def generate_handouts(args):
|
|
55
56
|
_, resourcedir = get_source_dirs()
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
toml.loads(read_file(os.path.join(resourcedir, f"labels_{args.language}.toml")))
|
|
58
|
+
with open(
|
|
59
|
+
os.path.join(resourcedir, f"regexes_{args.language}.json"), encoding="utf8"
|
|
60
|
+
) as f:
|
|
61
|
+
regexes = json.load(f)
|
|
62
|
+
handout_re_text = (
|
|
63
|
+
"\\[" + regexes["handout_short"] + ".+?:( |\n)(?P<handout_text>.+?)\\]"
|
|
58
64
|
)
|
|
59
65
|
handout_re = re.compile(
|
|
60
|
-
|
|
61
|
-
+ labels["question_labels"]["handout_short"]
|
|
62
|
-
+ ".+?:( |\n)(?P<handout_text>.+?)\\]",
|
|
66
|
+
handout_re_text,
|
|
63
67
|
flags=re.DOTALL,
|
|
64
68
|
)
|
|
65
69
|
|
|
@@ -82,9 +86,9 @@ def generate_handouts(args):
|
|
|
82
86
|
if img:
|
|
83
87
|
try:
|
|
84
88
|
parsed_img = parseimg(img[0][1])
|
|
85
|
-
except:
|
|
89
|
+
except Exception as e:
|
|
86
90
|
print(
|
|
87
|
-
f"Image file for question {q['number']} not found, add it by hand"
|
|
91
|
+
f"Image file for question {q['number']} not found, add it by hand (exception {type(e)} {e})"
|
|
88
92
|
)
|
|
89
93
|
continue
|
|
90
94
|
else:
|
chgksuite/handouter/installer.py
CHANGED
|
File without changes
|
chgksuite/handouter/runner.py
CHANGED
|
@@ -5,15 +5,19 @@ import shutil
|
|
|
5
5
|
import subprocess
|
|
6
6
|
import time
|
|
7
7
|
|
|
8
|
+
import toml
|
|
8
9
|
from watchdog.events import FileSystemEventHandler
|
|
9
10
|
from watchdog.observers import Observer
|
|
10
11
|
|
|
12
|
+
from chgksuite.common import get_source_dirs
|
|
11
13
|
from chgksuite.handouter.gen import generate_handouts
|
|
12
14
|
from chgksuite.handouter.pack import pack_handouts
|
|
13
15
|
from chgksuite.handouter.installer import get_tectonic_path, install_tectonic
|
|
14
16
|
from chgksuite.handouter.tex_internals import (
|
|
17
|
+
EDGE_DASHED,
|
|
18
|
+
EDGE_NONE,
|
|
19
|
+
EDGE_SOLID,
|
|
15
20
|
GREYTEXT,
|
|
16
|
-
GREYTEXT_LANGS,
|
|
17
21
|
HEADER,
|
|
18
22
|
IMG,
|
|
19
23
|
IMGWIDTH,
|
|
@@ -29,6 +33,10 @@ class HandoutGenerator:
|
|
|
29
33
|
|
|
30
34
|
def __init__(self, args):
|
|
31
35
|
self.args = args
|
|
36
|
+
_, resourcedir = get_source_dirs()
|
|
37
|
+
self.labels = toml.loads(
|
|
38
|
+
read_file(os.path.join(resourcedir, f"labels_{args.language}.toml"))
|
|
39
|
+
)
|
|
32
40
|
self.blocks = [self.get_header()]
|
|
33
41
|
|
|
34
42
|
def get_header(self):
|
|
@@ -51,11 +59,33 @@ class HandoutGenerator:
|
|
|
51
59
|
return parse_handouts(contents)
|
|
52
60
|
|
|
53
61
|
def generate_for_question(self, question_num):
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
handout_text = self.labels["general"]["handout_for_question"].format(
|
|
63
|
+
question_num
|
|
56
64
|
)
|
|
65
|
+
return GREYTEXT.replace("<GREYTEXT>", handout_text)
|
|
66
|
+
|
|
67
|
+
def make_tikzbox(self, block, edges=None, ext=None):
|
|
68
|
+
"""
|
|
69
|
+
Create a TikZ box with configurable edge styles and extensions.
|
|
70
|
+
edges is a dict with keys 'top', 'bottom', 'left', 'right'
|
|
71
|
+
values are EDGE_DASHED or EDGE_SOLID
|
|
72
|
+
ext is a dict with edge extensions to close gaps at boundaries
|
|
73
|
+
"""
|
|
74
|
+
if edges is None:
|
|
75
|
+
edges = {
|
|
76
|
+
"top": EDGE_DASHED,
|
|
77
|
+
"bottom": EDGE_DASHED,
|
|
78
|
+
"left": EDGE_DASHED,
|
|
79
|
+
"right": EDGE_DASHED,
|
|
80
|
+
}
|
|
81
|
+
if ext is None:
|
|
82
|
+
ext = {
|
|
83
|
+
"top": ("0pt", "0pt"),
|
|
84
|
+
"bottom": ("0pt", "0pt"),
|
|
85
|
+
"left": ("0pt", "0pt"),
|
|
86
|
+
"right": ("0pt", "0pt"),
|
|
87
|
+
}
|
|
57
88
|
|
|
58
|
-
def make_tikzbox(self, block):
|
|
59
89
|
if block.get("no_center"):
|
|
60
90
|
align = ""
|
|
61
91
|
else:
|
|
@@ -73,19 +103,206 @@ class HandoutGenerator:
|
|
|
73
103
|
.replace("<ALIGN>", align)
|
|
74
104
|
.replace("<TEXTWIDTH>", textwidth)
|
|
75
105
|
.replace("<FONTSIZE>", fontsize)
|
|
106
|
+
.replace("<TOP>", edges["top"])
|
|
107
|
+
.replace("<BOTTOM>", edges["bottom"])
|
|
108
|
+
.replace("<LEFT>", edges["left"])
|
|
109
|
+
.replace("<RIGHT>", edges["right"])
|
|
110
|
+
.replace("<TOP_EXT_L>", ext["top"][0])
|
|
111
|
+
.replace("<TOP_EXT_R>", ext["top"][1])
|
|
112
|
+
.replace("<BOTTOM_EXT_L>", ext["bottom"][0])
|
|
113
|
+
.replace("<BOTTOM_EXT_R>", ext["bottom"][1])
|
|
114
|
+
.replace("<LEFT_EXT_T>", ext["left"][0])
|
|
115
|
+
.replace("<LEFT_EXT_B>", ext["left"][1])
|
|
116
|
+
.replace("<RIGHT_EXT_T>", ext["right"][0])
|
|
117
|
+
.replace("<RIGHT_EXT_B>", ext["right"][1])
|
|
76
118
|
)
|
|
77
119
|
|
|
78
120
|
def get_page_width(self):
|
|
79
121
|
return self.args.paperwidth - self.args.margin_left - self.args.margin_right - 2
|
|
80
122
|
|
|
123
|
+
def get_cut_direction(self, columns, num_rows, handouts_per_team):
|
|
124
|
+
"""
|
|
125
|
+
Determine whether to cut vertically or horizontally.
|
|
126
|
+
Returns (direction, team_size) where:
|
|
127
|
+
- direction is 'vertical', 'horizontal', or None
|
|
128
|
+
- team_size is the number of columns (vertical) or rows (horizontal) per team
|
|
129
|
+
|
|
130
|
+
Falls back to None if handouts can't be evenly divided into teams.
|
|
131
|
+
"""
|
|
132
|
+
total = columns * num_rows
|
|
133
|
+
|
|
134
|
+
# Check if total handouts can be evenly divided
|
|
135
|
+
if total % handouts_per_team != 0:
|
|
136
|
+
return None, None
|
|
137
|
+
|
|
138
|
+
num_teams = total // handouts_per_team
|
|
139
|
+
if num_teams < 2:
|
|
140
|
+
return None, None # Only 1 team, no cuts needed
|
|
141
|
+
|
|
142
|
+
# Try vertical layout (teams as column groups)
|
|
143
|
+
# Each team gets team_cols columns × all rows
|
|
144
|
+
if handouts_per_team % num_rows == 0:
|
|
145
|
+
team_cols = handouts_per_team // num_rows
|
|
146
|
+
if columns % team_cols == 0:
|
|
147
|
+
return "vertical", team_cols
|
|
148
|
+
|
|
149
|
+
# Try horizontal layout (teams as row groups)
|
|
150
|
+
# Each team gets all columns × team_rows rows
|
|
151
|
+
if handouts_per_team % columns == 0:
|
|
152
|
+
team_rows = handouts_per_team // columns
|
|
153
|
+
if num_rows % team_rows == 0:
|
|
154
|
+
return "horizontal", team_rows
|
|
155
|
+
|
|
156
|
+
return None, None
|
|
157
|
+
|
|
158
|
+
def get_edge_styles(
|
|
159
|
+
self, row_idx, col_idx, num_rows, columns, cut_direction, team_size
|
|
160
|
+
):
|
|
161
|
+
"""
|
|
162
|
+
Determine edge styles and extensions for a box at position (row_idx, col_idx).
|
|
163
|
+
Outer edges of team rectangles are solid (thicker), inner edges are dashed.
|
|
164
|
+
Extensions are used to close gaps in ALL solid lines.
|
|
165
|
+
Duplicate dashed edges are skipped to avoid double lines.
|
|
166
|
+
|
|
167
|
+
team_size is the number of columns (vertical) or rows (horizontal) per team.
|
|
168
|
+
"""
|
|
169
|
+
# Default: all dashed, no extension
|
|
170
|
+
edges = {
|
|
171
|
+
"top": EDGE_DASHED,
|
|
172
|
+
"bottom": EDGE_DASHED,
|
|
173
|
+
"left": EDGE_DASHED,
|
|
174
|
+
"right": EDGE_DASHED,
|
|
175
|
+
}
|
|
176
|
+
ext = {
|
|
177
|
+
"top": ("0pt", "0pt"),
|
|
178
|
+
"bottom": ("0pt", "0pt"),
|
|
179
|
+
"left": ("0pt", "0pt"),
|
|
180
|
+
"right": ("0pt", "0pt"),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Gap sizes (half of spacing to extend into)
|
|
184
|
+
h_gap = "0.75mm" # half of SPACE (1.5mm)
|
|
185
|
+
v_gap = "0.5mm" # half of vspace (1mm)
|
|
186
|
+
|
|
187
|
+
# Helper functions to check if position is at a team boundary
|
|
188
|
+
def is_at_right_team_boundary():
|
|
189
|
+
"""Is this box at the right edge of its team (but not at grid edge)?"""
|
|
190
|
+
if cut_direction != "vertical" or not team_size:
|
|
191
|
+
return False
|
|
192
|
+
return (col_idx + 1) % team_size == 0 and col_idx < columns - 1
|
|
193
|
+
|
|
194
|
+
def is_at_left_team_boundary():
|
|
195
|
+
"""Is this box at the left edge of its team (but not at grid edge)?"""
|
|
196
|
+
if cut_direction != "vertical" or not team_size:
|
|
197
|
+
return False
|
|
198
|
+
return col_idx % team_size == 0 and col_idx > 0
|
|
199
|
+
|
|
200
|
+
def is_at_bottom_team_boundary():
|
|
201
|
+
"""Is this box at the bottom edge of its team (but not at grid edge)?"""
|
|
202
|
+
if cut_direction != "horizontal" or not team_size:
|
|
203
|
+
return False
|
|
204
|
+
return (row_idx + 1) % team_size == 0 and row_idx < num_rows - 1
|
|
205
|
+
|
|
206
|
+
def is_at_top_team_boundary():
|
|
207
|
+
"""Is this box at the top edge of its team (but not at grid edge)?"""
|
|
208
|
+
if cut_direction != "horizontal" or not team_size:
|
|
209
|
+
return False
|
|
210
|
+
return row_idx % team_size == 0 and row_idx > 0
|
|
211
|
+
|
|
212
|
+
# Determine which edges are solid
|
|
213
|
+
# Only apply solid edges if we have a valid cut direction
|
|
214
|
+
# Otherwise fall back to all-dashed (default)
|
|
215
|
+
if cut_direction is not None:
|
|
216
|
+
# Outer edges of the entire grid
|
|
217
|
+
if row_idx == 0:
|
|
218
|
+
edges["top"] = EDGE_SOLID
|
|
219
|
+
if row_idx == num_rows - 1:
|
|
220
|
+
edges["bottom"] = EDGE_SOLID
|
|
221
|
+
if col_idx == 0:
|
|
222
|
+
edges["left"] = EDGE_SOLID
|
|
223
|
+
if col_idx == columns - 1:
|
|
224
|
+
edges["right"] = EDGE_SOLID
|
|
225
|
+
|
|
226
|
+
# Team boundary edges
|
|
227
|
+
if is_at_right_team_boundary():
|
|
228
|
+
edges["right"] = EDGE_SOLID
|
|
229
|
+
if is_at_left_team_boundary():
|
|
230
|
+
edges["left"] = EDGE_SOLID
|
|
231
|
+
if is_at_bottom_team_boundary():
|
|
232
|
+
edges["bottom"] = EDGE_SOLID
|
|
233
|
+
if is_at_top_team_boundary():
|
|
234
|
+
edges["top"] = EDGE_SOLID
|
|
235
|
+
|
|
236
|
+
# Skip duplicate dashed edges (to avoid double lines between adjacent boxes)
|
|
237
|
+
if edges["left"] == EDGE_DASHED and col_idx > 0:
|
|
238
|
+
edges["left"] = EDGE_NONE
|
|
239
|
+
|
|
240
|
+
if edges["top"] == EDGE_DASHED and row_idx > 0:
|
|
241
|
+
edges["top"] = EDGE_NONE
|
|
242
|
+
|
|
243
|
+
# Calculate extensions for solid edges to close gaps
|
|
244
|
+
# But don't extend into team boundary gaps!
|
|
245
|
+
|
|
246
|
+
if edges["top"] == EDGE_SOLID:
|
|
247
|
+
at_left_boundary = is_at_left_team_boundary()
|
|
248
|
+
ext_left = "-" + h_gap if col_idx > 0 and not at_left_boundary else "0pt"
|
|
249
|
+
at_right_boundary = is_at_right_team_boundary()
|
|
250
|
+
ext_right = (
|
|
251
|
+
h_gap if col_idx < columns - 1 and not at_right_boundary else "0pt"
|
|
252
|
+
)
|
|
253
|
+
ext["top"] = (ext_left, ext_right)
|
|
254
|
+
|
|
255
|
+
if edges["bottom"] == EDGE_SOLID:
|
|
256
|
+
at_left_boundary = is_at_left_team_boundary()
|
|
257
|
+
ext_left = "-" + h_gap if col_idx > 0 and not at_left_boundary else "0pt"
|
|
258
|
+
at_right_boundary = is_at_right_team_boundary()
|
|
259
|
+
ext_right = (
|
|
260
|
+
h_gap if col_idx < columns - 1 and not at_right_boundary else "0pt"
|
|
261
|
+
)
|
|
262
|
+
ext["bottom"] = (ext_left, ext_right)
|
|
263
|
+
|
|
264
|
+
if edges["left"] == EDGE_SOLID:
|
|
265
|
+
at_top_boundary = is_at_top_team_boundary()
|
|
266
|
+
ext_top = v_gap if row_idx > 0 and not at_top_boundary else "0pt"
|
|
267
|
+
at_bottom_boundary = is_at_bottom_team_boundary()
|
|
268
|
+
ext_bottom = (
|
|
269
|
+
"-" + v_gap
|
|
270
|
+
if row_idx < num_rows - 1 and not at_bottom_boundary
|
|
271
|
+
else "0pt"
|
|
272
|
+
)
|
|
273
|
+
ext["left"] = (ext_top, ext_bottom)
|
|
274
|
+
|
|
275
|
+
if edges["right"] == EDGE_SOLID:
|
|
276
|
+
at_top_boundary = is_at_top_team_boundary()
|
|
277
|
+
ext_top = v_gap if row_idx > 0 and not at_top_boundary else "0pt"
|
|
278
|
+
at_bottom_boundary = is_at_bottom_team_boundary()
|
|
279
|
+
ext_bottom = (
|
|
280
|
+
"-" + v_gap
|
|
281
|
+
if row_idx < num_rows - 1 and not at_bottom_boundary
|
|
282
|
+
else "0pt"
|
|
283
|
+
)
|
|
284
|
+
ext["right"] = (ext_top, ext_bottom)
|
|
285
|
+
|
|
286
|
+
return edges, ext
|
|
287
|
+
|
|
81
288
|
def generate_regular_block(self, block_):
|
|
82
289
|
block = block_.copy()
|
|
83
290
|
if not (block.get("image") or block.get("text")):
|
|
84
291
|
return
|
|
85
292
|
columns = block["columns"]
|
|
86
|
-
|
|
293
|
+
num_rows = block.get("rows") or 1
|
|
294
|
+
handouts_per_team = block.get("handouts_per_team") or 3
|
|
295
|
+
|
|
296
|
+
# Determine cut direction
|
|
297
|
+
cut_direction, cut_after = self.get_cut_direction(
|
|
298
|
+
columns, num_rows, handouts_per_team
|
|
299
|
+
)
|
|
300
|
+
if self.args.debug:
|
|
301
|
+
print(f"cut_direction: {cut_direction}, cut_after: {cut_after}")
|
|
302
|
+
|
|
303
|
+
spaces = columns - 1
|
|
87
304
|
boxwidth = self.args.boxwidth or round(
|
|
88
|
-
(self.get_page_width() - spaces * self.SPACE) /
|
|
305
|
+
(self.get_page_width() - spaces * self.SPACE) / columns,
|
|
89
306
|
3,
|
|
90
307
|
)
|
|
91
308
|
total_width = boxwidth * columns + spaces * self.SPACE
|
|
@@ -98,7 +315,6 @@ class HandoutGenerator:
|
|
|
98
315
|
r"\setlength{\boxwidth}{<Q>mm}%".replace("<Q>", str(boxwidth)),
|
|
99
316
|
r"\setlength{\boxwidthinner}{<Q>mm}%".replace("<Q>", str(boxwidthinner)),
|
|
100
317
|
]
|
|
101
|
-
rows = []
|
|
102
318
|
contents = []
|
|
103
319
|
if block.get("image"):
|
|
104
320
|
img_qwidth = block.get("resize_image") or 1.0
|
|
@@ -113,10 +329,18 @@ class HandoutGenerator:
|
|
|
113
329
|
block["centering"] = ""
|
|
114
330
|
else:
|
|
115
331
|
block["centering"] = "\\centering"
|
|
116
|
-
|
|
332
|
+
|
|
333
|
+
rows = []
|
|
334
|
+
for row_idx in range(num_rows):
|
|
335
|
+
row_boxes = []
|
|
336
|
+
for col_idx in range(columns):
|
|
337
|
+
edges, ext = self.get_edge_styles(
|
|
338
|
+
row_idx, col_idx, num_rows, columns, cut_direction, cut_after
|
|
339
|
+
)
|
|
340
|
+
row_boxes.append(self.make_tikzbox(block, edges, ext))
|
|
117
341
|
row = (
|
|
118
342
|
TIKZBOX_START.replace("<CENTERING>", block["centering"])
|
|
119
|
-
+ "\n".join(
|
|
343
|
+
+ "\n".join(row_boxes)
|
|
120
344
|
+ TIKZBOX_END
|
|
121
345
|
)
|
|
122
346
|
rows.append(row)
|
|
@@ -141,7 +365,7 @@ class HandoutGenerator:
|
|
|
141
365
|
|
|
142
366
|
def process_file(args, file_dir, bn):
|
|
143
367
|
tex_contents = HandoutGenerator(args).generate()
|
|
144
|
-
tex_path = os.path.join(file_dir, f"{bn}_{args.
|
|
368
|
+
tex_path = os.path.join(file_dir, f"{bn}_{args.language}.tex")
|
|
145
369
|
write_file(tex_path, tex_contents)
|
|
146
370
|
|
|
147
371
|
tectonic_path = get_tectonic_path()
|
|
@@ -13,33 +13,32 @@ HEADER = r"""
|
|
|
13
13
|
\begin{document}
|
|
14
14
|
\fontsize{14pt}{16pt}\selectfont
|
|
15
15
|
\setlength\parindent{0pt}
|
|
16
|
-
\tikzstyle{box}=[
|
|
16
|
+
\tikzstyle{box}=[rectangle, inner sep=<TIKZ_MM>mm]
|
|
17
17
|
\raggedright
|
|
18
18
|
\raggedbottom
|
|
19
19
|
""".strip()
|
|
20
20
|
|
|
21
21
|
GREYTEXT = r"""{\fontsize{9pt}{11pt}\selectfont \textcolor{gray}{<GREYTEXT>}}"""
|
|
22
22
|
|
|
23
|
-
GREYTEXT_LANGS = {
|
|
24
|
-
"by": "Да пытаньня {}",
|
|
25
|
-
"en": "Handout for question {}",
|
|
26
|
-
"kz": "{}-сұрақтың үлестіру материалы",
|
|
27
|
-
"ro": "Material care urmează a fi distribuit pentru întrebarea {}",
|
|
28
|
-
"ru": "К вопросу {}",
|
|
29
|
-
"sr": "Materijal za deljenje uz pitanje {}",
|
|
30
|
-
"ua": "До запитання {}",
|
|
31
|
-
"uz": "{} саволга тарқатма материал",
|
|
32
|
-
}
|
|
33
|
-
|
|
34
23
|
TIKZBOX_START = r"""{<CENTERING>
|
|
35
24
|
"""
|
|
36
25
|
|
|
37
26
|
TIKZBOX_INNER = r"""
|
|
38
27
|
\begin{tikzpicture}
|
|
39
|
-
\node[box, minimum width=\boxwidth<TEXTWIDTH><ALIGN>] {<FONTSIZE><CONTENTS>\par};
|
|
28
|
+
\node[box, minimum width=\boxwidth<TEXTWIDTH><ALIGN>] (b) {<FONTSIZE><CONTENTS>\par};
|
|
29
|
+
\useasboundingbox (b.south west) rectangle (b.north east);
|
|
30
|
+
\draw[<TOP>] ([xshift=<TOP_EXT_L>]b.north west) -- ([xshift=<TOP_EXT_R>]b.north east);
|
|
31
|
+
\draw[<BOTTOM>] ([xshift=<BOTTOM_EXT_L>]b.south west) -- ([xshift=<BOTTOM_EXT_R>]b.south east);
|
|
32
|
+
\draw[<LEFT>] ([yshift=<LEFT_EXT_T>]b.north west) -- ([yshift=<LEFT_EXT_B>]b.south west);
|
|
33
|
+
\draw[<RIGHT>] ([yshift=<RIGHT_EXT_T>]b.north east) -- ([yshift=<RIGHT_EXT_B>]b.south east);
|
|
40
34
|
\end{tikzpicture}
|
|
41
35
|
""".strip()
|
|
42
36
|
|
|
37
|
+
# Line styles for box edges
|
|
38
|
+
EDGE_SOLID = "line width=0.8pt"
|
|
39
|
+
EDGE_DASHED = "dashed"
|
|
40
|
+
EDGE_NONE = "draw=none" # Don't draw this edge (to avoid double dashed lines)
|
|
41
|
+
|
|
43
42
|
TIKZBOX_END = "\n}"
|
|
44
43
|
|
|
45
44
|
IMG = r"""\includegraphics<IMGWIDTH>{<IMGPATH>}"""
|
chgksuite/lastdir
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/Users/pecheny/chgksuite1/tmpz_2mf3o8/tmptke0lqfv
|