workpytools 0.1.4__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 (48) hide show
  1. workpytools/__init__.py +1 -0
  2. workpytools/cli.py +76 -0
  3. workpytools/common/__init__.py +0 -0
  4. workpytools/common/browser_preview.py +40 -0
  5. workpytools/common/clipboard.py +318 -0
  6. workpytools/common/clustering.py +85 -0
  7. workpytools/common/config.py +44 -0
  8. workpytools/common/embedding.py +101 -0
  9. workpytools/common/help_gen.py +542 -0
  10. workpytools/common/logging.py +12 -0
  11. workpytools/common/markdown_html.py +15 -0
  12. workpytools/common/outline_parse.py +81 -0
  13. workpytools/common/output.py +71 -0
  14. workpytools/common/powerpoint.py +43 -0
  15. workpytools/common/shape_cluster.py +75 -0
  16. workpytools/common/table_shapes.py +157 -0
  17. workpytools/common/tabular.py +248 -0
  18. workpytools/common/textfile.py +90 -0
  19. workpytools/common/walk.py +133 -0
  20. workpytools/data/__init__.py +0 -0
  21. workpytools/data/synonym.tsv +3 -0
  22. workpytools/data/user.dic +2 -0
  23. workpytools/processing/__init__.py +0 -0
  24. workpytools/processing/base.py +23 -0
  25. workpytools/processing/clipfmt.py +143 -0
  26. workpytools/processing/clipmd.py +237 -0
  27. workpytools/processing/clipview.py +194 -0
  28. workpytools/processing/cwc.py +427 -0
  29. workpytools/processing/denoise.py +81 -0
  30. workpytools/processing/help.py +47 -0
  31. workpytools/processing/ikko.py +205 -0
  32. workpytools/processing/kukiri.py +94 -0
  33. workpytools/processing/lsdir.py +218 -0
  34. workpytools/processing/mdtsv.py +177 -0
  35. workpytools/processing/mokuji.py +91 -0
  36. workpytools/processing/nagasa.py +94 -0
  37. workpytools/processing/outline.py +85 -0
  38. workpytools/processing/profiler.py +277 -0
  39. workpytools/processing/seiretsu.py +111 -0
  40. workpytools/processing/tbl.py +358 -0
  41. workpytools/processing/touka.py +52 -0
  42. workpytools/processing/vv.py +147 -0
  43. workpytools-0.1.4.dist-info/METADATA +458 -0
  44. workpytools-0.1.4.dist-info/RECORD +48 -0
  45. workpytools-0.1.4.dist-info/WHEEL +5 -0
  46. workpytools-0.1.4.dist-info/entry_points.txt +20 -0
  47. workpytools-0.1.4.dist-info/licenses/LICENSE +21 -0
  48. workpytools-0.1.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import csv
5
+ import io
6
+ import logging
7
+ import re
8
+
9
+ from workpytools.common.clipboard import copy_text_to_clipboard, get_clipboard_text
10
+ from workpytools.processing.base import Processor
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ _PIPE_ESCAPE = "\x00PIPE\x00" # セル分割前にエスケープ済みパイプを退避する一時トークン
15
+
16
+
17
+ def _is_markdown_table(text: str) -> bool:
18
+ lines = [line for line in text.split("\n") if line.strip()]
19
+ if len(lines) < 2:
20
+ return False
21
+ if not all(line.strip().startswith("|") and line.strip().endswith("|") for line in lines):
22
+ return False
23
+ separator = lines[1].strip()
24
+ return bool(re.fullmatch(r"\|[\s:|-]+\|", separator)) and "-" in separator
25
+
26
+
27
+ def _is_tsv(text: str) -> bool:
28
+ lines = [line for line in text.split("\n") if line.strip()]
29
+ if not lines:
30
+ return False
31
+ return all("\t" in line for line in lines)
32
+
33
+
34
+ def _split_table_row(line: str) -> list[str]:
35
+ stripped = line.strip().strip("|")
36
+ escaped = stripped.replace("\\|", _PIPE_ESCAPE)
37
+ cells = [cell.strip().replace(_PIPE_ESCAPE, "|") for cell in escaped.split("|")]
38
+ return cells
39
+
40
+
41
+ def _is_separator_row(line: str) -> bool:
42
+ stripped = line.strip()
43
+ return bool(re.fullmatch(r"\|[\s:|-]+\|", stripped)) and "-" in stripped
44
+
45
+
46
+ def markdown_table_to_tsv(text: str) -> str:
47
+ lines = [line for line in text.split("\n") if line.strip()]
48
+
49
+ table_count = 0
50
+ data_rows: list[list[str]] = []
51
+ br_replaced = False
52
+
53
+ for line in lines:
54
+ if _is_separator_row(line):
55
+ table_count += 1
56
+ continue
57
+ cells = _split_table_row(line)
58
+ new_cells = []
59
+ for cell in cells:
60
+ if "<br>" in cell or "<br/>" in cell or "<br />" in cell:
61
+ br_replaced = True
62
+ cell = re.sub(r"<br\s*/?>", " ", cell)
63
+ new_cells.append(cell)
64
+ data_rows.append(new_cells)
65
+
66
+ if table_count > 1:
67
+ logger.info("表が%d個検出されたため、1つのTSVに連結します", table_count)
68
+ if br_replaced:
69
+ logger.warning("セル内の改行(<br>)はTSVで表現できないため、スペースに置き換えました")
70
+
71
+ out = io.StringIO()
72
+ writer = csv.writer(out, delimiter="\t", lineterminator="\n")
73
+ for row in data_rows:
74
+ writer.writerow(row)
75
+ return out.getvalue().rstrip("\n")
76
+
77
+
78
+ def tsv_to_markdown_table(text: str) -> str:
79
+ reader = csv.reader(io.StringIO(text), delimiter="\t")
80
+ rows = [row for row in reader if row]
81
+
82
+ br_replaced = False
83
+ processed_rows = []
84
+ for row in rows:
85
+ new_row = []
86
+ for cell in row:
87
+ cell = cell.replace("\\", "\\\\").replace("|", "\\|")
88
+ if "\n" in cell:
89
+ br_replaced = True
90
+ cell = cell.replace("\n", "<br>")
91
+ new_row.append(cell)
92
+ processed_rows.append(new_row)
93
+
94
+ max_cols = max((len(row) for row in processed_rows), default=0)
95
+ for row in processed_rows:
96
+ while len(row) < max_cols:
97
+ row.append("")
98
+
99
+ if br_replaced:
100
+ logger.warning("セル内の改行を <br> に置き換えました")
101
+
102
+ lines = []
103
+ if processed_rows:
104
+ header = processed_rows[0]
105
+ lines.append("| " + " | ".join(header) + " |")
106
+ lines.append("| " + " | ".join(["---"] * max_cols) + " |")
107
+ for row in processed_rows[1:]:
108
+ lines.append("| " + " | ".join(row) + " |")
109
+
110
+ return "\n".join(lines)
111
+
112
+
113
+ class MdtsvProcessor(Processor):
114
+ """Convert clipboard contents between a Markdown table and TSV.
115
+
116
+ Direction is auto-detected from the plain-text content (CF_UNICODETEXT
117
+ only; CF_HTML is never consulted, since Excel's HTML is noisier than
118
+ reconstructing from its TSV payload).
119
+ """
120
+
121
+ name = "mdtsv"
122
+ help = "クリップボードのMarkdownの表とTSVを相互変換する"
123
+
124
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
125
+ group = parser.add_mutually_exclusive_group()
126
+ group.add_argument(
127
+ "--to-tsv", action="store_true", help="入力をMarkdownの表とみなしTSVに変換する"
128
+ )
129
+ group.add_argument(
130
+ "--to-table", action="store_true", help="入力をTSVとみなしMarkdownの表に変換する"
131
+ )
132
+
133
+ def run(self, args: argparse.Namespace) -> int:
134
+ direction = self._resolve_direction(args)
135
+
136
+ source_text = get_clipboard_text()
137
+
138
+ if direction == "to_tsv":
139
+ result = markdown_table_to_tsv(source_text)
140
+ label = "Markdownの表 → TSV"
141
+ else:
142
+ result = tsv_to_markdown_table(source_text)
143
+ label = "TSV → Markdownの表"
144
+
145
+ if not result.strip():
146
+ raise SystemExit("変換結果が空です")
147
+
148
+ copy_text_to_clipboard(result)
149
+ print(f"{label} に変換しました")
150
+ return 0
151
+
152
+ def _resolve_direction(self, args: argparse.Namespace) -> str:
153
+ try:
154
+ source_text = get_clipboard_text()
155
+ except Exception as exc:
156
+ raise SystemExit("変換できるテキストがありません") from exc
157
+
158
+ if args.to_tsv:
159
+ if not _is_markdown_table(source_text):
160
+ raise SystemExit("入力をMarkdownの表として解釈できません")
161
+ return "to_tsv"
162
+
163
+ if args.to_table:
164
+ logger.info("--to-table指定: タブの有無にかかわらずTSVとして扱います")
165
+ return "to_table"
166
+
167
+ if _is_markdown_table(source_text):
168
+ logger.info("Markdownの表を検出したため 表 → TSV に変換します")
169
+ return "to_tsv"
170
+ if _is_tsv(source_text):
171
+ logger.info("TSVを検出したため TSV → 表 に変換します")
172
+ return "to_table"
173
+
174
+ raise SystemExit(
175
+ "入力をMarkdownの表ともTSVとも解釈できません"
176
+ "(Markdownの表全体、またはタブ区切りのテキストをコピーしてください)"
177
+ )
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import re
6
+
7
+ from workpytools.common.clipboard import copy_text_to_clipboard
8
+ from workpytools.common.powerpoint import (
9
+ NoActivePresentationError,
10
+ PowerPointNotRunningError,
11
+ get_active_presentation,
12
+ get_running_powerpoint,
13
+ )
14
+ from workpytools.common.textfile import to_crlf
15
+ from workpytools.processing.base import Processor
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ _NEWLINE_RE = re.compile(r"\r\n|\r|\n")
20
+
21
+
22
+ class MokujiProcessor(Processor):
23
+ """Collect a title per slide across the active presentation and copy
24
+ the list to the clipboard, one title per line.
25
+ """
26
+
27
+ name = "mokuji"
28
+ help = "全スライドのタイトルを一覧化してクリップボードにコピーする"
29
+
30
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
31
+ pass
32
+
33
+ def run(self, args: argparse.Namespace) -> int:
34
+ try:
35
+ app = get_running_powerpoint()
36
+ except PowerPointNotRunningError as exc:
37
+ raise SystemExit(
38
+ "PowerPointでプレゼンテーションを開いた状態で実行してください"
39
+ ) from exc
40
+
41
+ try:
42
+ presentation = get_active_presentation(app)
43
+ except NoActivePresentationError as exc:
44
+ raise SystemExit(
45
+ "PowerPointでプレゼンテーションを開いた状態で実行してください"
46
+ ) from exc
47
+
48
+ try:
49
+ slide_count = presentation.Slides.Count
50
+ if slide_count == 0:
51
+ print("対象のスライドがありません")
52
+ return 0
53
+
54
+ titles = [
55
+ self._extract_title(presentation.Slides.Item(i))
56
+ for i in range(1, slide_count + 1)
57
+ ]
58
+ except Exception as exc:
59
+ raise SystemExit(f"PowerPointの操作中にエラーが発生しました: {exc}") from exc
60
+
61
+ text = to_crlf("\n".join(titles))
62
+ copy_text_to_clipboard(text)
63
+ logger.info("%d枚のスライドから%d件のタイトルを抽出しました", slide_count, len(titles))
64
+ print(f"{len(titles)}件のタイトルをコピーしました")
65
+ return 0
66
+
67
+ def _extract_title(self, slide: object) -> str:
68
+ if slide.Shapes.HasTitle: # type: ignore[attr-defined]
69
+ title_text = slide.Shapes.Title.TextFrame.TextRange.Text # type: ignore[attr-defined]
70
+ if title_text.strip():
71
+ return self._flatten(title_text)
72
+
73
+ fallback = self._topmost_text(slide)
74
+ return self._flatten(fallback) if fallback else ""
75
+
76
+ def _topmost_text(self, slide: object) -> str | None:
77
+ best_top: float | None = None
78
+ best_text: str | None = None
79
+ for shape in slide.Shapes: # type: ignore[attr-defined]
80
+ if not getattr(shape, "HasTextFrame", False):
81
+ continue
82
+ text = shape.TextFrame.TextRange.Text
83
+ if not text or not text.strip():
84
+ continue
85
+ if best_top is None or shape.Top < best_top:
86
+ best_top = shape.Top
87
+ best_text = text
88
+ return best_text
89
+
90
+ def _flatten(self, text: str) -> str:
91
+ return _NEWLINE_RE.sub(" ", text).strip()
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+
6
+ from workpytools.common.powerpoint import (
7
+ NoActivePresentationError,
8
+ PowerPointNotRunningError,
9
+ get_active_presentation,
10
+ get_running_powerpoint,
11
+ )
12
+ from workpytools.processing.base import Processor
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ _SELECTION_SHAPES = 2 # ppSelectionShapes
17
+ _PP_AUTO_SIZE_NONE = 0
18
+
19
+
20
+ class NagasaProcessor(Processor):
21
+ """Unify the width and height of selected shapes to the max found
22
+ among them, resizing around each shape's own center so relative
23
+ positions (e.g. after seiretsu) aren't disturbed.
24
+ """
25
+
26
+ name = "nagasa"
27
+ help = "選択したシェイプの幅・高さを最大値に統一する"
28
+
29
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
30
+ pass
31
+
32
+ def run(self, args: argparse.Namespace) -> int:
33
+ try:
34
+ app = get_running_powerpoint()
35
+ except PowerPointNotRunningError as exc:
36
+ raise SystemExit(
37
+ "PowerPointでプレゼンテーションを開いた状態で実行してください"
38
+ ) from exc
39
+
40
+ try:
41
+ get_active_presentation(app)
42
+ except NoActivePresentationError as exc:
43
+ raise SystemExit(
44
+ "PowerPointでプレゼンテーションを開いた状態で実行してください"
45
+ ) from exc
46
+
47
+ try:
48
+ selection = app.ActiveWindow.Selection
49
+ except Exception as exc:
50
+ raise SystemExit(f"選択状態の取得中にエラーが発生しました: {exc}") from exc
51
+
52
+ if selection.Type != _SELECTION_SHAPES:
53
+ raise SystemExit("対象を選択してから実行してください(2つ以上選択が必要です)")
54
+
55
+ shape_range = selection.ShapeRange
56
+ shapes = [shape_range.Item(i) for i in range(1, shape_range.Count + 1)]
57
+
58
+ if len(shapes) < 2:
59
+ raise SystemExit("対象を選択してから実行してください(2つ以上選択が必要です)")
60
+
61
+ max_width = max(s.Width for s in shapes)
62
+ max_height = max(s.Height for s in shapes)
63
+
64
+ try:
65
+ app.StartNewUndoEntry()
66
+ for shape in shapes:
67
+ self._disable_autosize(shape)
68
+ center_x = shape.Left + shape.Width / 2
69
+ center_y = shape.Top + shape.Height / 2
70
+ shape.Width = max_width
71
+ shape.Height = max_height
72
+ shape.Left = center_x - max_width / 2
73
+ shape.Top = center_y - max_height / 2
74
+ except Exception as exc:
75
+ raise SystemExit(
76
+ f"PowerPointの操作中にエラーが発生しました(Ctrl+Zで開始前の状態に戻せます): {exc}"
77
+ ) from exc
78
+
79
+ logger.info(
80
+ "対象シェイプ数=%d 統一サイズ=幅%.1f 高さ%.1f", len(shapes), max_width, max_height
81
+ )
82
+ print(
83
+ f"{len(shapes)}個のシェイプのサイズを統一しました"
84
+ f"(幅{max_width:.1f}pt, 高さ{max_height:.1f}pt)"
85
+ )
86
+ return 0
87
+
88
+ def _disable_autosize(self, shape: object) -> None:
89
+ if not getattr(shape, "HasTextFrame", False):
90
+ return
91
+ try:
92
+ shape.TextFrame.AutoSize = _PP_AUTO_SIZE_NONE # type: ignore[attr-defined]
93
+ except Exception:
94
+ pass
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+
6
+ from workpytools.common.clipboard import ClipboardTextError, get_clipboard_text
7
+ from workpytools.common.outline_parse import OutlineItem, parse_outline
8
+ from workpytools.common.powerpoint import (
9
+ NoActivePresentationError,
10
+ PowerPointNotRunningError,
11
+ get_active_presentation,
12
+ get_running_powerpoint,
13
+ )
14
+ from workpytools.processing.base import Processor
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ _LAYOUT_TITLE_AND_CONTENT = 2 # ppLayoutText
19
+
20
+
21
+ class OutlineProcessor(Processor):
22
+ """Turn a clipboard outline into slides appended to the active
23
+ PowerPoint presentation. Only adds slides -- table-of-contents
24
+ generation is a separate command (mokuji).
25
+ """
26
+
27
+ name = "outline"
28
+ help = "クリップボードのアウトラインからPowerPointにスライドを追加する"
29
+
30
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
31
+ pass
32
+
33
+ def run(self, args: argparse.Namespace) -> int:
34
+ try:
35
+ text = get_clipboard_text()
36
+ except ClipboardTextError as exc:
37
+ raise SystemExit(str(exc)) from exc
38
+
39
+ items = parse_outline(text)
40
+ if not items:
41
+ raise SystemExit("アウトラインとして解釈できる内容がありません")
42
+
43
+ presentation, created_new = self._get_or_create_presentation()
44
+
45
+ try:
46
+ for item in items:
47
+ self._add_slide(presentation, item)
48
+ except Exception as exc:
49
+ raise SystemExit(f"PowerPointの操作中にエラーが発生しました: {exc}") from exc
50
+
51
+ logger.info(
52
+ "%d件のスライドを追加しました(%s)",
53
+ len(items),
54
+ "新規プレゼンテーション" if created_new else "既存のプレゼンテーションに追記",
55
+ )
56
+ print(f"{len(items)}件のスライドを追加しました")
57
+ return 0
58
+
59
+ def _get_or_create_presentation(self) -> tuple[object, bool]:
60
+ try:
61
+ app = get_running_powerpoint()
62
+ except PowerPointNotRunningError:
63
+ import win32com.client
64
+
65
+ app = win32com.client.Dispatch("PowerPoint.Application")
66
+ app.Visible = True
67
+ presentation = app.Presentations.Add()
68
+ logger.info("PowerPointを新規起動し、新規プレゼンテーションを作成しました")
69
+ return presentation, True
70
+
71
+ try:
72
+ presentation = get_active_presentation(app)
73
+ except NoActivePresentationError:
74
+ presentation = app.Presentations.Add()
75
+ logger.info("既存のPowerPointに新規プレゼンテーションを作成しました")
76
+ return presentation, True
77
+
78
+ logger.info("既存のプレゼンテーションに追記します")
79
+ return presentation, False
80
+
81
+ def _add_slide(self, presentation: object, item: OutlineItem) -> None:
82
+ index = presentation.Slides.Count + 1 # type: ignore[attr-defined]
83
+ slide = presentation.Slides.Add(index, _LAYOUT_TITLE_AND_CONTENT) # type: ignore[attr-defined]
84
+ slide.Shapes.Placeholders(1).TextFrame.TextRange.Text = item.title
85
+ slide.Shapes.Placeholders(2).TextFrame.TextRange.Text = item.body.replace("\n", "\r")