markdocx 3.0.0__tar.gz → 5.0.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markdocx
3
- Version: 3.0.0
3
+ Version: 5.0.0
4
4
  Summary: Convert AI-generated Markdown textbooks to polished DOCX with native math equations and syntax-highlighted code
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "markdocx"
3
- version = "3.0.0"
3
+ version = "5.0.0"
4
4
  description = "Convert AI-generated Markdown textbooks to polished DOCX with native math equations and syntax-highlighted code"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -665,6 +665,12 @@ caption: Hình 8: Quy trình ETL
665
665
  - Khuyến nghị dùng `[Bắt đầu]` và `[Kết thúc]` cho bước đầu và cuối
666
666
  - Giữ quy trình tối đa **8 bước** để dễ đọc
667
667
  - Dùng `title:` và `caption:` để đặt nhãn
668
+ - **Mỗi bước viết ngắn gọn, tối đa 10-12 ký tự** (ví dụ: `Thu nhận ảnh`, `Xử lý ảnh`)
669
+ - Nếu tên bước dài hơn 10 ký tự, hệ thống sẽ tự động xuống dòng
670
+ - **Bố cục ngang (`horizontal`):** viết tên bước **thật ngắn** (2-3 từ), không dùng câu dài
671
+ - ✅ Đúng: `(Thu nhận ảnh)`, `(Trích xuất)`, `(Phân loại)`
672
+ - ❌ Sai: `(Thu nhận và tiền xử lý ảnh đầu vào)`, `(Trích xuất đặc trưng từ dữ liệu)`
673
+ - **Bố cục dọc (`vertical`):** có thể viết dài hơn (tối đa 25-30 ký tự)
668
674
 
669
675
  ---
670
676
 
@@ -702,6 +702,12 @@ caption: Figure 8: ETL pipeline
702
702
  - `[Start]` and `[End]` are recommended for the first and last steps
703
703
  - Keep workflows to **8 steps or fewer** for readability
704
704
  - Use `title:` and `caption:` for labeling
705
+ - **Keep each step label short — max 10-12 characters** (e.g., `Read Data`, `Transform`)
706
+ - Text longer than 10 characters will automatically wrap to 2 lines
707
+ - **Horizontal layout (`horizontal`):** use **very short** step labels (2-3 words max)
708
+ - ✅ Correct: `(Read Data)`, `(Transform)`, `(Classify)`
709
+ - ❌ Wrong: `(Read and preprocess input images)`, `(Extract features from data)`
710
+ - **Vertical layout (`vertical`):** may use longer labels (up to 25-30 characters)
705
711
 
706
712
  ---
707
713
 
@@ -7,5 +7,5 @@ with native math equations and syntax-highlighted code.
7
7
 
8
8
  from markdocx.core import convert_file, convert_directory
9
9
 
10
- __version__ = "3.0.0"
10
+ __version__ = "5.0.0"
11
11
  __all__ = ["convert_file", "convert_directory"]
@@ -731,42 +731,76 @@ def _render_workflow(code: str):
731
731
  n = len(steps)
732
732
  is_vertical = direction.startswith("v")
733
733
 
734
- # Calculate figure size
734
+ # ── Measure text to compute adaptive box sizes ──────────────
735
+ def _wrap_text(text, max_chars):
736
+ """Wrap text into multiple lines if too long."""
737
+ if len(text) <= max_chars:
738
+ return text
739
+ words = text.split()
740
+ lines = []
741
+ current = ""
742
+ for word in words:
743
+ test = f"{current} {word}".strip() if current else word
744
+ if len(test) <= max_chars:
745
+ current = test
746
+ else:
747
+ if current:
748
+ lines.append(current)
749
+ current = word
750
+ if current:
751
+ lines.append(current)
752
+ return "\n".join(lines) if lines else text
753
+
754
+ # Find longest text to size boxes appropriately
755
+ max_text_len = max(len(s.get("text", "")) for s in steps)
756
+
735
757
  if is_vertical:
736
- fig_w = max(5, 6)
737
- fig_h = max(3, n * 1.5 + 1.5)
758
+ # Vertical: boxes can be wide
759
+ max_chars_per_line = 30
760
+ box_w = max(3.0, min(6.0, max_text_len * 0.18 + 1.0))
761
+ box_h = 0.9
762
+ fontsize_normal = 12
763
+ fontsize_decision = 11
764
+ spacing_y = 1.8 # center-to-center vertical distance
765
+
766
+ fig_w = max(6, box_w + 3.0)
767
+ fig_h = max(4, n * spacing_y + 2.0)
738
768
  else:
739
- fig_w = max(4, n * 2.5 + 1)
740
- fig_h = max(3, 4)
769
+ # Horizontal: wider boxes, bigger font, wrap early for readability
770
+ max_chars_per_line = 10
771
+ box_w = max(2.8, min(4.0, max_text_len * 0.18 + 1.2))
772
+ box_h = 1.2
773
+ fontsize_normal = 13
774
+ fontsize_decision = 12
775
+ spacing_x = box_w + 1.2 # center-to-center horizontal distance
776
+
777
+ fig_w = max(8, n * spacing_x + 2.5)
778
+ fig_h = max(4.0, 5.0)
741
779
 
742
780
  fig, ax = plt.subplots(figsize=(fig_w, fig_h))
743
- ax.set_aspect("equal")
781
+ # Do NOT use set_aspect("equal") — it distorts boxes
744
782
  ax.axis("off")
745
783
 
746
- # Shape dimensions
747
- box_w = 2.0
748
- box_h = 0.7
749
- spacing = 1.5 # center-to-center distance
750
-
751
- # Calculate positions
784
+ # ── Calculate positions ─────────────────────────────────────
752
785
  positions = []
753
786
  for i in range(n):
754
787
  if is_vertical:
755
788
  x = fig_w / 2
756
- y = fig_h - 1.0 - i * spacing
789
+ y = fig_h - 1.5 - i * spacing_y
757
790
  else:
758
- x = 1.5 + i * (box_w + 0.8)
791
+ x = 1.5 + i * spacing_x
759
792
  y = fig_h / 2
760
793
  positions.append((x, y))
761
794
 
762
795
  # Adjust axes limits
763
796
  all_x = [p[0] for p in positions]
764
797
  all_y = [p[1] for p in positions]
765
- margin = 2.0
766
- ax.set_xlim(min(all_x) - margin, max(all_x) + margin)
767
- ax.set_ylim(min(all_y) - margin, max(all_y) + margin)
798
+ pad_x = box_w / 2 + 1.5
799
+ pad_y = box_h + 1.5
800
+ ax.set_xlim(min(all_x) - pad_x, max(all_x) + pad_x)
801
+ ax.set_ylim(min(all_y) - pad_y, max(all_y) + pad_y)
768
802
 
769
- # Color scheme for step types
803
+ # ── Color scheme ────────────────────────────────────────────
770
804
  type_colors = {
771
805
  "terminal": ("#34A853", "white"),
772
806
  "start": ("#34A853", "white"),
@@ -776,17 +810,21 @@ def _render_workflow(code: str):
776
810
  "io": ("#7B61FF", "white"),
777
811
  }
778
812
 
779
- # Draw steps
813
+ # ── Draw steps ──────────────────────────────────────────────
780
814
  for i, step in enumerate(steps):
781
815
  x, y = positions[i]
782
816
  step_type = step.get("type", "process")
783
- text = step.get("text", "")
817
+ raw_text = step.get("text", "")
818
+ display_text = _wrap_text(raw_text, max_chars_per_line)
784
819
  bg_color, text_color = type_colors.get(step_type, ("#4285F4", "white"))
785
820
 
821
+ # Count lines for height adjustment
822
+ n_lines = display_text.count("\n") + 1
823
+ effective_h = max(box_h, box_h + (n_lines - 1) * 0.3)
824
+
786
825
  if step_type == "decision":
787
- # Diamond shape
788
- diamond_w = box_w * 0.7
789
- diamond_h = box_h * 1.2
826
+ diamond_w = box_w * 0.55
827
+ diamond_h = effective_h * 0.9
790
828
  diamond = plt.Polygon([
791
829
  (x, y + diamond_h),
792
830
  (x + diamond_w, y),
@@ -795,68 +833,82 @@ def _render_workflow(code: str):
795
833
  ], closed=True, facecolor=bg_color, edgecolor="#333333",
796
834
  linewidth=1.5, zorder=3)
797
835
  ax.add_patch(diamond)
798
- ax.text(x, y, text, ha="center", va="center",
799
- fontsize=9, fontweight="bold", color=text_color, zorder=4,
800
- wrap=True)
836
+ ax.text(x, y, display_text, ha="center", va="center",
837
+ fontsize=fontsize_decision, fontweight="bold",
838
+ color=text_color, zorder=4, linespacing=1.2)
801
839
 
802
840
  elif step_type in ("terminal", "start", "end"):
803
- # Rounded rectangle (stadium shape)
804
841
  patch = FancyBboxPatch(
805
- (x - box_w / 2, y - box_h / 2), box_w, box_h,
842
+ (x - box_w / 2, y - effective_h / 2), box_w, effective_h,
806
843
  boxstyle="round,pad=0.15", facecolor=bg_color,
807
844
  edgecolor="#333333", linewidth=1.5, zorder=3
808
845
  )
809
846
  ax.add_patch(patch)
810
- ax.text(x, y, text, ha="center", va="center",
811
- fontsize=10, fontweight="bold", color=text_color, zorder=4)
847
+ ax.text(x, y, display_text, ha="center", va="center",
848
+ fontsize=fontsize_normal, fontweight="bold",
849
+ color=text_color, zorder=4, linespacing=1.2)
812
850
 
813
851
  elif step_type == "io":
814
- # Parallelogram
815
- skew = 0.2
852
+ skew = 0.25
816
853
  pgram = plt.Polygon([
817
- (x - box_w / 2 + skew, y + box_h / 2),
818
- (x + box_w / 2 + skew, y + box_h / 2),
819
- (x + box_w / 2 - skew, y - box_h / 2),
820
- (x - box_w / 2 - skew, y - box_h / 2)
854
+ (x - box_w / 2 + skew, y + effective_h / 2),
855
+ (x + box_w / 2 + skew, y + effective_h / 2),
856
+ (x + box_w / 2 - skew, y - effective_h / 2),
857
+ (x - box_w / 2 - skew, y - effective_h / 2)
821
858
  ], closed=True, facecolor=bg_color, edgecolor="#333333",
822
859
  linewidth=1.5, zorder=3)
823
860
  ax.add_patch(pgram)
824
- ax.text(x, y, text, ha="center", va="center",
825
- fontsize=10, fontweight="bold", color=text_color, zorder=4)
861
+ ax.text(x, y, display_text, ha="center", va="center",
862
+ fontsize=fontsize_normal, fontweight="bold",
863
+ color=text_color, zorder=4, linespacing=1.2)
826
864
 
827
865
  else: # process
828
- # Rectangle
829
866
  patch = FancyBboxPatch(
830
- (x - box_w / 2, y - box_h / 2), box_w, box_h,
867
+ (x - box_w / 2, y - effective_h / 2), box_w, effective_h,
831
868
  boxstyle="square,pad=0.1", facecolor=bg_color,
832
869
  edgecolor="#333333", linewidth=1.5, zorder=3
833
870
  )
834
871
  ax.add_patch(patch)
835
- ax.text(x, y, text, ha="center", va="center",
836
- fontsize=10, fontweight="bold", color=text_color, zorder=4)
872
+ ax.text(x, y, display_text, ha="center", va="center",
873
+ fontsize=fontsize_normal, fontweight="bold",
874
+ color=text_color, zorder=4, linespacing=1.2)
837
875
 
838
- # Draw arrows between steps
876
+ # ── Draw arrows between steps ───────────────────────────────
839
877
  for i in range(n - 1):
840
878
  x1, y1 = positions[i]
841
879
  x2, y2 = positions[i + 1]
842
880
 
843
- step_type = steps[i].get("type", "process")
881
+ cur_type = steps[i].get("type", "process")
882
+ next_type = steps[i + 1].get("type", "process")
883
+
884
+ # Recompute effective heights for arrow endpoints
885
+ cur_text = _wrap_text(steps[i].get("text", ""), max_chars_per_line)
886
+ next_text = _wrap_text(steps[i + 1].get("text", ""), max_chars_per_line)
887
+ cur_h = max(box_h, box_h + (cur_text.count("\n")) * 0.3)
888
+ next_h = max(box_h, box_h + (next_text.count("\n")) * 0.3)
844
889
 
845
890
  if is_vertical:
846
- start_y = y1 - box_h / 2 - 0.05
847
- end_y = y2 + box_h / 2 + 0.05
848
- if step_type == "decision":
849
- start_y = y1 - box_h * 1.2 - 0.05
850
- next_type = steps[i + 1].get("type", "process")
891
+ if cur_type == "decision":
892
+ start_y = y1 - cur_h * 0.9 - 0.08
893
+ else:
894
+ start_y = y1 - cur_h / 2 - 0.08
851
895
  if next_type == "decision":
852
- end_y = y2 + box_h * 1.2 + 0.05
896
+ end_y = y2 + next_h * 0.9 + 0.08
897
+ else:
898
+ end_y = y2 + next_h / 2 + 0.08
853
899
  ax.annotate("", xy=(x2, end_y), xytext=(x1, start_y),
854
900
  arrowprops=dict(arrowstyle="-|>", color="#555555",
855
901
  lw=2, mutation_scale=15),
856
902
  zorder=2)
857
903
  else:
858
- start_x = x1 + box_w / 2 + 0.05
859
- end_x = x2 - box_w / 2 - 0.05
904
+ if cur_type == "decision":
905
+ start_x = x1 + box_w * 0.55 + 0.08
906
+ else:
907
+ start_x = x1 + box_w / 2 + 0.08
908
+ if next_type == "decision":
909
+ end_x = x2 - box_w * 0.55 - 0.08
910
+ else:
911
+ end_x = x2 - box_w / 2 - 0.08
860
912
  ax.annotate("", xy=(end_x, y2), xytext=(start_x, y1),
861
913
  arrowprops=dict(arrowstyle="-|>", color="#555555",
862
914
  lw=2, mutation_scale=15),
@@ -158,7 +158,7 @@ wheels = [
158
158
 
159
159
  [[package]]
160
160
  name = "markdocx"
161
- version = "2.0.0"
161
+ version = "3.0.0"
162
162
  source = { editable = "." }
163
163
  dependencies = [
164
164
  { name = "latex2mathml" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes