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.
Files changed (52) hide show
  1. chgksuite/_html2md.py +90 -0
  2. chgksuite/cli.py +21 -8
  3. chgksuite/common.py +16 -12
  4. chgksuite/composer/__init__.py +9 -7
  5. chgksuite/composer/chgksuite_parser.py +16 -7
  6. chgksuite/composer/composer_common.py +14 -5
  7. chgksuite/composer/db.py +1 -2
  8. chgksuite/composer/docx.py +29 -8
  9. chgksuite/composer/latex.py +1 -2
  10. chgksuite/composer/lj.py +1 -2
  11. chgksuite/composer/{reddit.py → markdown.py} +35 -25
  12. chgksuite/composer/openquiz.py +2 -3
  13. chgksuite/composer/pptx.py +2 -2
  14. chgksuite/composer/telegram.py +2 -1
  15. chgksuite/handouter/gen.py +11 -7
  16. chgksuite/handouter/installer.py +0 -0
  17. chgksuite/handouter/runner.py +234 -10
  18. chgksuite/handouter/tex_internals.py +12 -13
  19. chgksuite/lastdir +1 -0
  20. chgksuite/parser.py +32 -33
  21. chgksuite/parser_db.py +4 -6
  22. chgksuite/resources/labels_az.toml +22 -0
  23. chgksuite/resources/labels_by.toml +1 -2
  24. chgksuite/resources/labels_by_tar.toml +1 -2
  25. chgksuite/resources/labels_en.toml +1 -2
  26. chgksuite/resources/labels_kz_cyr.toml +1 -2
  27. chgksuite/resources/labels_ru.toml +1 -2
  28. chgksuite/resources/labels_sr.toml +1 -2
  29. chgksuite/resources/labels_ua.toml +1 -2
  30. chgksuite/resources/labels_uz.toml +0 -3
  31. chgksuite/resources/labels_uz_cyr.toml +1 -2
  32. chgksuite/resources/regexes_az.json +17 -0
  33. chgksuite/resources/regexes_by.json +3 -2
  34. chgksuite/resources/regexes_by_tar.json +17 -0
  35. chgksuite/resources/regexes_en.json +3 -2
  36. chgksuite/resources/regexes_kz_cyr.json +3 -2
  37. chgksuite/resources/regexes_ru.json +3 -2
  38. chgksuite/resources/regexes_sr.json +3 -2
  39. chgksuite/resources/regexes_ua.json +3 -2
  40. chgksuite/resources/regexes_uz.json +16 -0
  41. chgksuite/resources/regexes_uz_cyr.json +3 -2
  42. chgksuite/trello.py +8 -9
  43. chgksuite/typotools.py +9 -8
  44. chgksuite/version.py +1 -1
  45. {chgksuite-0.26.1.dist-info → chgksuite-0.27.0.dist-info}/METADATA +10 -19
  46. chgksuite-0.27.0.dist-info/RECORD +63 -0
  47. {chgksuite-0.26.1.dist-info → chgksuite-0.27.0.dist-info}/WHEEL +1 -2
  48. chgksuite/composer/telegram_parser.py +0 -230
  49. chgksuite-0.26.1.dist-info/RECORD +0 -59
  50. chgksuite-0.26.1.dist-info/top_level.txt +0 -1
  51. {chgksuite-0.26.1.dist-info → chgksuite-0.27.0.dist-info}/entry_points.txt +0 -0
  52. {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 RedditExporter(BaseExporter):
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 reddityapper(self, e):
17
+ def markdownyapper(self, e):
19
18
  if isinstance(e, str):
20
- return self.reddit_element_layout(e)
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.reddit_element_layout(e)
22
+ return self.markdown_element_layout(e)
24
23
  else:
25
- return " \n".join([self.reddit_element_layout(x) for x in e])
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 redditformat(self, s):
39
+ def markdownformat(self, s):
41
40
  res = ""
42
41
  for run in self.parse_4s_elem(s):
43
- if run[0] in ("", "hyperlink"):
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
- res += "[картинка]({})".format(imglink)
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 reddit_element_layout(self, e):
64
+ def markdown_element_layout(self, e):
61
65
  res = ""
62
66
  if isinstance(e, str):
63
- res = self.redditformat(e)
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.reddit_element_layout(x))
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 reddit_format_element(self, pair):
78
+ def markdown_format_element(self, pair):
75
79
  if pair[0] == "Question":
76
- return self.reddit_format_question(pair[1])
80
+ return self.markdown_format_question(pair[1])
77
81
 
78
- def reddit_format_question(self, q):
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.reddityapper(q["question"]),
87
+ self.markdownyapper(q["question"]),
84
88
  )
85
89
  if "number" not in q:
86
90
  self.qcount += 1
87
- res += "__Ответ:__ >!{} \n".format(self.reddityapper(q["answer"]))
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.reddityapper(q["zachet"]))
97
+ res += "__Зачёт:__ {} \n".format(self.markdownyapper(q["zachet"]))
90
98
  if "nezachet" in q:
91
- res += "__Незачёт:__ {} \n".format(self.reddityapper(q["nezachet"]))
99
+ res += "__Незачёт:__ {} \n".format(self.markdownyapper(q["nezachet"]))
92
100
  if "comment" in q:
93
- res += "__Комментарий:__ {} \n".format(self.reddityapper(q["comment"]))
101
+ res += "__Комментарий:__ {} \n".format(self.markdownyapper(q["comment"]))
94
102
  if "source" in q:
95
- res += "__Источник:__ {} \n".format(self.reddityapper(q["source"]))
103
+ res += "__Источник:__ {} \n".format(self.markdownyapper(q["source"]))
96
104
  if "author" in q:
97
- res += "!<\n__Автор:__ {} \n".format(self.reddityapper(q["author"]))
105
+ res += "{}\n__Автор:__ {} \n".format(
106
+ spoiler_end, self.markdownyapper(q["author"])
107
+ )
98
108
  else:
99
- res += "!<\n"
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.reddit_format_element(pair)
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 codecs.open(outfile, "w", "utf8") as f:
119
+ with open(outfile, "w", encoding="utf-8") as f:
110
120
  f.write(text)
111
121
  self.logger.info("Output: {}".format(outfile))
@@ -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.labels["question_labels"]["handout_short"]
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 codecs.open(outfilename, "w", "utf8") as f:
177
+ with open(outfilename, "w", encoding="utf-8") as f:
179
178
  f.write(json.dumps(result, indent=2, ensure_ascii=False))
@@ -121,13 +121,13 @@ class PptxExporter(BaseExporter):
121
121
  replace_spaces=True,
122
122
  do_not_remove_accents=False,
123
123
  ):
124
- hs = self.labels["question_labels"]["handout_short"]
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.labels)
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")
@@ -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 | str | None:
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:
@@ -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
- labels = toml.loads(
57
- read_file(os.path.join(resourcedir, f"labels_{args.lang}.toml"))
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:
File without changes
@@ -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
- return GREYTEXT.replace(
55
- "<GREYTEXT>", GREYTEXT_LANGS[self.args.lang].format(question_num)
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
- spaces = block["columns"] - 1
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) / block["columns"],
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
- for _ in range(block.get("rows") or 1):
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([self.make_tikzbox(block)] * block["columns"])
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.lang}.tex")
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}=[draw, dashed, rectangle, inner sep=<TIKZ_MM>mm]
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