AtCoderStudyBooster 0.3__py3-none-any.whl → 0.3.2__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.
atcdr/generate.py CHANGED
@@ -2,105 +2,94 @@ import json
2
2
  import os
3
3
  import re
4
4
 
5
+ import rich_click as click
5
6
  from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.syntax import Syntax
6
9
 
7
- from atcdr.test import (
8
- LabeledTestCaseResult,
9
- ResultStatus,
10
- create_testcases_from_html,
11
- judge_code_from,
12
- render_results,
13
- )
14
- from atcdr.util.execute import execute_files
10
+ from atcdr.test import ResultStatus, TestRunner, create_renderable_test_info
11
+ from atcdr.util.fileops import add_file_selector
15
12
  from atcdr.util.filetype import (
16
- FILE_EXTENSIONS,
17
- Filename,
18
- Lang,
19
- lang2str,
20
- str2lang,
13
+ FILE_EXTENSIONS,
14
+ Filename,
15
+ Lang,
16
+ lang2str,
17
+ str2lang,
21
18
  )
22
- from atcdr.util.gpt import ChatGPT, set_api_key
23
- from atcdr.util.problem import make_problem_markdown
19
+ from atcdr.util.gpt import ChatGPT, Model, set_api_key
20
+ from atcdr.util.parse import ProblemHTML
24
21
 
25
22
 
26
23
  def get_code_from_gpt_output(output: str) -> str:
27
- pattern = re.compile(r'```(?:\w+)?\s*(.*?)\s*```', re.DOTALL)
28
- match = pattern.search(output)
29
- return match.group(1) if match else ''
30
-
31
-
32
- def render_result_for_GPT(lresult: LabeledTestCaseResult) -> str:
33
- output = f'{lresult.label} of Test:\n'
34
- result = lresult.result
35
- testcase = lresult.testcase
36
-
37
- if result.passed == ResultStatus.AC:
38
- output += 'Accepted !! \n'
39
-
40
- elif result.passed == ResultStatus.WA:
41
- output += (
42
- f'Wrong Answer\n'
43
- f'Output:\n{result.output}\n'
44
- f'Expected Output:\n{testcase.output}\n'
45
- )
46
-
47
- elif result.passed == ResultStatus.RE:
48
- output += f'[RE] Runtime Error\n Output:\n{result.output}'
49
-
50
- elif result.passed == ResultStatus.TLE:
51
- output += '[TLE] Time Limit Exceeded\n'
52
-
53
- elif result.passed == ResultStatus.CE:
54
- output += f'[CE] Compile Error\n Output:\n{result.output}'
55
-
56
- elif result.passed == ResultStatus.MLE:
57
- output += '[ME] Memory Limit Exceeded\n'
58
-
59
- return output
60
-
61
-
62
- def generate_code(file: Filename, lang: Lang) -> None:
63
- console = Console()
64
- with open(file, 'r') as f:
65
- html_content = f.read()
66
- md = make_problem_markdown(html_content, 'en')
67
-
68
- if set_api_key() is None:
69
- return
70
- gpt = ChatGPT(
71
- system_prompt=f"""You are an excellent programmer. You solve problems in competitive programming.When a user provides you with a problem from a programming contest called AtCoder, including the Problem,Constraints, Input, Output, Input Example, and Output Example, please carefully consider these and solve the problem.Make sure that your output code block contains no more than two blocks. Pay close attention to the Input, Input Example, Output, and Output Example.Create the solution in {lang2str(lang)}.""",
72
- )
73
- with console.status(f'{gpt.model.value}がコードを生成しています...'):
74
- reply = gpt.tell(md)
75
-
76
- code = get_code_from_gpt_output(reply)
77
-
78
- saved_filename = (
79
- os.path.splitext(file)[0] + f'_by_{gpt.model.value}' + FILE_EXTENSIONS[lang]
80
- )
81
- with open(saved_filename, 'w') as f:
82
- console.print(
83
- f'[green][+][/green] {gpt.model.value} の出力したコードを保存しました:{f.name}'
84
- )
85
- f.write(code)
86
-
87
- console.print(f'[info] AI利用にかかったAPIコスト: {gpt.sum_cost}')
24
+ pattern = re.compile(r'```(?:\w+)?\s*(.*?)\s*```', re.DOTALL)
25
+ match = pattern.search(output)
26
+ return match.group(1) if match else ''
27
+
28
+
29
+ def render_result_for_GPT(
30
+ test: TestRunner,
31
+ ) -> tuple[str, bool]:
32
+ results = list(test)
33
+
34
+ match test.info.summary:
35
+ case ResultStatus.AC:
36
+ return 'Accepted', True
37
+ case ResultStatus.CE:
38
+ return f'Compile Error \n {test.info.compiler_message}', False
39
+ case _:
40
+ message_for_gpt = ''.join(
41
+ f'\n{result.label} => {result.result.passed.value}\nInput :\n{result.testcase.input}\nOutput :\n{result.result.output}\nExpected :\n{result.testcase.output}\n'
42
+ if result.result.passed == ResultStatus.WA
43
+ else f'\n{result.label} => {result.result.passed.value}\nInput :\n{result.testcase.input}\nOutput :\n{result.result.output}\n'
44
+ for result in results
45
+ )
46
+ return message_for_gpt, False
47
+
48
+
49
+ def generate_code(file: Filename, lang: Lang, model: Model) -> None:
50
+ console = Console()
51
+ with open(file, 'r') as f:
52
+ html_content = f.read()
53
+ md = ProblemHTML(html_content).make_problem_markdown('en')
54
+
55
+ if set_api_key() is None:
56
+ return
57
+ gpt = ChatGPT(
58
+ system_prompt=f"""You are an excellent programmer. You solve problems in competitive programming.When a user provides you with a problem from a programming contest called AtCoder, including the Problem,Constraints, Input, Output, Input Example, and Output Example, please carefully consider these and solve the problem.Make sure that your output code block contains no more than two blocks. Pay close attention to the Input, Input Example, Output, and Output Example.Create the solution in {lang2str(lang)}.""",
59
+ model=model,
60
+ )
61
+ with console.status(f'コード生成中 (by {gpt.model.value})'):
62
+ reply = gpt.tell(md)
63
+
64
+ code = get_code_from_gpt_output(reply)
65
+ console.print('[green][+][/green] コードの生成に成功しました. ')
66
+ console.rule(f'{gpt.model.value}による{lang2str(lang)}コード')
67
+ console.print(Syntax(code=code, lexer=lang2str(lang)))
68
+
69
+ saved_filename = (
70
+ os.path.splitext(file)[0] + f'_by_{gpt.model.value}' + FILE_EXTENSIONS[lang]
71
+ )
72
+ with open(saved_filename, 'w') as f:
73
+ console.print(
74
+ f'[green][+][/green] {gpt.model.value} の出力したコードを保存しました:{f.name}'
75
+ )
76
+ f.write(code)
88
77
 
89
78
 
90
79
  def generate_template(file: Filename, lang: Lang) -> None:
91
- console = Console()
92
- with open(file, 'r') as f:
93
- html_content = f.read()
94
- md = make_problem_markdown(html_content, 'en')
95
-
96
- if set_api_key() is None:
97
- return
98
- gpt = ChatGPT(
99
- system_prompt='You are a highly skilled programmer. Your role is to create a template code for competitive programming.',
100
- temperature=0.0,
101
- )
102
-
103
- propmpt = f"""
80
+ console = Console()
81
+ with open(file, 'r') as f:
82
+ html_content = f.read()
83
+ md = ProblemHTML(html_content).make_problem_markdown('en')
84
+
85
+ if set_api_key() is None:
86
+ return
87
+ gpt = ChatGPT(
88
+ system_prompt='You are a highly skilled programmer. Your role is to create a template code for competitive programming.',
89
+ temperature=0.0,
90
+ )
91
+
92
+ propmpt = f"""
104
93
  The user will provide a problem from a programming contest called AtCoder. This problem will include the Problem Statement, Constraints, Input, Output, Input Example, and Output Example. You should focus on the Constraints and Input sections to create the template in {lang2str(lang)}.
105
94
 
106
95
  - First, create the part of the code that handles input. Then, you should read ###Input Block and ###Constraints Block.
@@ -109,112 +98,105 @@ The user will provide a problem from a programming contest called AtCoder. This
109
98
 
110
99
  You must not solve the problem. Please faithfully reproduce the variable names defined in the problem.
111
100
  """
112
- with console.status(f'{lang2str(lang)}のテンプレートを生成しています...'):
113
- reply = gpt.tell(md + propmpt)
114
- code = get_code_from_gpt_output(reply)
115
-
116
- savaed_filename = os.path.splitext(file)[0] + FILE_EXTENSIONS[lang]
117
- with open(savaed_filename, 'x') as f:
118
- console.print(
119
- f'[green][+][/green] テンプレートファイルを作成 :{savaed_filename}'
120
- )
121
- f.write(code)
122
-
123
- console.print(f'[info] AI利用にかかったAPIコスト: {gpt.sum_cost}')
124
-
125
-
126
- def solve_problem(file: Filename, lang: Lang) -> None:
127
- console = Console()
128
- with open(file, 'r') as f:
129
- html_content = f.read()
130
- md = make_problem_markdown(html_content, 'en')
131
- labeled_cases = create_testcases_from_html(html_content)
132
-
133
- if set_api_key() is None:
134
- return
135
- gpt = ChatGPT(
136
- system_prompt=f"""You are a brilliant programmer. Your task is to solve an AtCoder problem. AtCoder is a platform that hosts programming competitions where participants write programs to solve algorithmic challenges.Please solve the problem in {lang2str(lang)}.""",
137
- )
138
-
139
- file_without_ext = os.path.splitext(file)[0]
140
-
141
- for i in range(1, 4):
142
- with console.status(f'{i}回目のコード生成 (by {gpt.model.value})...'):
143
- test_report = ''
144
- if i == 1:
145
- reply = gpt.tell(md)
146
- else:
147
- reply = gpt.tell(f"""The following is the test report for the code you provided:
148
- {test_report}
149
- Please provide an updated version of the code in {lang2str(lang)}.""")
150
-
151
- code = get_code_from_gpt_output(reply)
152
-
153
- saved_filename = (
154
- f'{i}_'
155
- + file_without_ext
156
- + f'_by_{gpt.model.value}'
157
- + FILE_EXTENSIONS[lang]
158
- )
159
- with open(saved_filename, 'w') as f:
160
- console.print(
161
- f'[green][+][/green] {gpt.model.value} の出力したコードを保存しました:{f.name}'
162
- )
163
- f.write(code)
164
-
165
- labeled_results = judge_code_from(labeled_cases, saved_filename)
166
- test_report = '\n'.join(
167
- render_result_for_GPT(lresult) for lresult in labeled_results
168
- )
169
-
170
- console.rule(f'{i}回目のコード生成でのテスト結果')
171
- render_results(saved_filename, labeled_results)
172
-
173
- if all(
174
- labeled_result.result.passed == ResultStatus.AC
175
- for labeled_result in labeled_results
176
- ):
177
- console.print('[green]コードのテストに成功![/green]')
178
- break
179
-
180
- with open(
181
- 'log_'
182
- + file_without_ext
183
- + f'_by_{gpt.model.value}'
184
- + FILE_EXTENSIONS[Lang.JSON],
185
- 'w',
186
- ) as f:
187
- console.print(
188
- f'[green][+][/green] {gpt.model.value}の出力のログを保存しました:{f.name}'
189
- )
190
- f.write(json.dumps(gpt.messages, indent=2))
191
- console.print(f'AI利用にかかったAPIコスト:{gpt.sum_cost}')
192
- return
193
-
194
-
195
- def generate(
196
- *source: str,
197
- lang: str = 'Python',
198
- without_test: bool = False,
199
- template: bool = False,
200
- ) -> None:
201
- la = str2lang(lang)
202
-
203
- if template:
204
- execute_files(
205
- *source,
206
- func=lambda file: generate_template(file, la),
207
- target_filetypes=[Lang.HTML],
208
- )
209
- elif without_test:
210
- execute_files(
211
- *source,
212
- func=lambda file: generate_code(file, la),
213
- target_filetypes=[Lang.HTML],
214
- )
215
- else:
216
- execute_files(
217
- *source,
218
- func=lambda file: solve_problem(file, la),
219
- target_filetypes=[Lang.HTML],
220
- )
101
+ with console.status(f'{lang2str(lang)}のテンプレートを生成しています...'):
102
+ reply = gpt.tell(md + propmpt)
103
+ code = get_code_from_gpt_output(reply)
104
+
105
+ savaed_filename = os.path.splitext(file)[0] + FILE_EXTENSIONS[lang]
106
+ with open(savaed_filename, 'x') as f:
107
+ console.print(
108
+ f'[green][+][/green] テンプレートファイルを作成 :{savaed_filename}'
109
+ )
110
+ f.write(code)
111
+
112
+
113
+ def solve_problem(file: Filename, lang: Lang, model: Model) -> None:
114
+ console = Console()
115
+ with open(file, 'r') as f:
116
+ html = ProblemHTML(f.read())
117
+
118
+ md = html.make_problem_markdown('en')
119
+ labeled_cases = html.load_labeled_testcase()
120
+
121
+ if set_api_key() is None:
122
+ return
123
+ gpt = ChatGPT(
124
+ system_prompt=f"""You are a brilliant programmer. Your task is to solve an AtCoder problem. AtCoder is a platform that hosts programming competitions where participants write programs to solve algorithmic challenges.Please solve the problem in {lang2str(lang)}.""",
125
+ model=model,
126
+ )
127
+
128
+ file_without_ext = os.path.splitext(file)[0]
129
+
130
+ for i in range(1, 4):
131
+ with console.status(f'{i}回目のコード生成中 (by {gpt.model.value})'):
132
+ if i == 1:
133
+ test_report = ''
134
+ reply = gpt.tell(md)
135
+ else:
136
+ prompt = f"""The following is the test report for the code you provided:
137
+ {test_report}
138
+ Please provide an updated version of the code in {lang2str(lang)}."""
139
+ console.print(
140
+ f'[green][+][/] 次のプロンプトを{gpt.model.value}に与え,再生成します'
141
+ )
142
+ console.print(Panel(prompt))
143
+ reply = gpt.tell(prompt)
144
+
145
+ code = get_code_from_gpt_output(reply)
146
+
147
+ saved_filename = (
148
+ f'{i}_'
149
+ + file_without_ext
150
+ + f'_by_{gpt.model.value}'
151
+ + FILE_EXTENSIONS[lang]
152
+ )
153
+ with open(saved_filename, 'w') as f:
154
+ console.print(f'[green][+][/] コードの生成に成功しました!:{f.name}')
155
+ f.write(code)
156
+
157
+ with console.status(
158
+ f'{gpt.model.value}が生成したコードをテスト中', spinner='circleHalves'
159
+ ):
160
+ test = TestRunner(saved_filename, labeled_cases)
161
+ test_report, is_ac = render_result_for_GPT(test)
162
+
163
+ console.print(create_renderable_test_info(test.info))
164
+
165
+ if is_ac:
166
+ console.print('[green][+][/] コードのテストに成功!')
167
+ break
168
+ else:
169
+ console.print('[red][-][/] コードのテストに失敗!')
170
+
171
+ with open(
172
+ 'log_'
173
+ + file_without_ext
174
+ + f'_by_{gpt.model.value}'
175
+ + FILE_EXTENSIONS[Lang.JSON],
176
+ 'w',
177
+ ) as f:
178
+ console.print(
179
+ f'[green][+][/] {gpt.model.value}の出力のログを保存しました:{f.name}'
180
+ )
181
+ f.write(json.dumps(gpt.messages, indent=2))
182
+ return
183
+
184
+
185
+ @click.command(short_help='コードを生成')
186
+ @add_file_selector('files', filetypes=[Lang.HTML])
187
+ @click.option('--lang', default='Python', help='出力するプログラミング言語')
188
+ @click.option('--model', default=Model.GPT41_MINI.value, help='使用するGPTモデル')
189
+ @click.option('--without-test', is_flag=True, help='テストケースを省略して生成')
190
+ @click.option('--template', is_flag=True, help='テンプレートを生成')
191
+ def generate(files, lang, model, without_test, template):
192
+ """HTMLファイルからコード生成またはテンプレート出力を行います。"""
193
+ la = str2lang(lang)
194
+ model_enum = Model(model)
195
+
196
+ for path in files:
197
+ if template:
198
+ generate_template(path, la)
199
+ elif without_test:
200
+ generate_code(path, la, model_enum)
201
+ else:
202
+ solve_problem(path, la, model_enum)
atcdr/login.py ADDED
@@ -0,0 +1,136 @@
1
+ import threading
2
+ import time
3
+
4
+ import rich_click as click
5
+ import webview
6
+ from requests import Session
7
+ from rich.console import Console
8
+
9
+ from atcdr.util.session import load_session, save_session, validate_session
10
+
11
+ ATCODER_LOGIN_URL = 'https://atcoder.jp/login'
12
+ ATCODER_HOME_URL = 'https://atcoder.jp/home'
13
+
14
+ console = Console()
15
+
16
+
17
+ @click.command(short_help='AtCoderへログイン')
18
+ def login() -> None:
19
+ """AtCoderへログインします."""
20
+ session = load_session()
21
+ if validate_session(session):
22
+ console.print('[green][+][/] すでにログインしています. ')
23
+ return
24
+
25
+ # Prompt in CLI
26
+ username = console.input('[cyan]ユーザー名: [/]').strip()
27
+ password = console.input('[cyan]パスワード: [/]').strip()
28
+
29
+ window = webview.create_window('AtCoder Login', ATCODER_LOGIN_URL, hidden=False)
30
+
31
+ def on_start():
32
+ js_fill = f"""
33
+ document.getElementById('username').value = '{username}';
34
+ document.getElementById('password').value = '{password}';
35
+ """
36
+ window.evaluate_js(js_fill)
37
+
38
+ def poll_and_submit():
39
+ with console.status(
40
+ 'キャプチャー認証を解決してください', spinner='circleHalves'
41
+ ):
42
+ while True:
43
+ try:
44
+ token = window.evaluate_js(
45
+ 'document.querySelector(\'input[name=\\"cf-turnstile-response\\"]\').value'
46
+ )
47
+ if token:
48
+ console.print('[green][+][/] ログインします')
49
+ window.evaluate_js(
50
+ "document.getElementById('submit').click();"
51
+ )
52
+ break
53
+ except Exception:
54
+ pass
55
+
56
+ time.sleep(0.5)
57
+
58
+ with console.status('ログインの結果の待機中...', spinner='circleHalves'):
59
+ while True:
60
+ try:
61
+ current_url = window.get_current_url()
62
+ except Exception:
63
+ current_url = None
64
+
65
+ if current_url and current_url.startswith(ATCODER_HOME_URL):
66
+ console.print('[green][+][/] ログイン成功!')
67
+
68
+ session = Session()
69
+ session = move_cookies_from_webview_to_session(window, session)
70
+ save_session(session)
71
+ window.destroy()
72
+ break
73
+
74
+ try:
75
+ err = window.evaluate_js(
76
+ 'Array.from(document.querySelectorAll('
77
+ '\'div.alert.alert-danger[role="alert"]\'))'
78
+ ".map(e=>e.textContent.trim()).filter(t=>t).join(' ')"
79
+ )
80
+ err = err.replace('\n', '').replace('\r', '').replace('\t', '')
81
+ except Exception:
82
+ err = ''
83
+
84
+ if err:
85
+ console.print(f'[red][-][/] エラー: {err}')
86
+ session = Session()
87
+ session = move_cookies_from_webview_to_session(window, session)
88
+ save_session(session)
89
+ window.destroy()
90
+ return
91
+
92
+ time.sleep(0.5)
93
+
94
+ t = threading.Thread(target=poll_and_submit, daemon=True)
95
+ t.start()
96
+
97
+ webview.start(on_start, private_mode=False)
98
+
99
+
100
+ def move_cookies_from_webview_to_session(
101
+ window: webview.Window, session: Session
102
+ ) -> Session:
103
+ cookie_list = window.get_cookies()
104
+ for cookie_obj in cookie_list:
105
+ for cookie_name, morsel in cookie_obj.items():
106
+ # morselからデータを取得
107
+ value = morsel.value
108
+
109
+ domain = morsel.get('domain')
110
+ if domain is None:
111
+ domain = '.atcoder.jp'
112
+
113
+ path = morsel.get('path', '/')
114
+ secure = 'secure' in morsel
115
+
116
+ expires = None # __NSTaggedDateオブジェクトを回避
117
+
118
+ http_only = 'httponly' in morsel
119
+
120
+ # HttpOnlyをrestに含める
121
+ rest = {}
122
+ if http_only:
123
+ rest['HttpOnly'] = True
124
+
125
+ # セッションにクッキーを設定
126
+ session.cookies.set(
127
+ name=cookie_name,
128
+ value=value,
129
+ domain=domain,
130
+ path=path,
131
+ secure=secure,
132
+ expires=expires, # Noneを渡す
133
+ rest=rest,
134
+ )
135
+
136
+ return session
atcdr/logout.py ADDED
@@ -0,0 +1,27 @@
1
+ import rich_click as click
2
+ import webview
3
+ from rich import print
4
+
5
+ from atcdr.util.session import delete_session, load_session, validate_session
6
+
7
+ ATCODER_LOGIN_URL = 'https://atcoder.jp/login'
8
+
9
+
10
+ @click.command(short_help='AtCoderへログアウト')
11
+ def logout() -> None:
12
+ """AtCoderからログアウトします."""
13
+ session = load_session()
14
+ if not validate_session(session):
15
+ print('[red][-][/] ログインしていません.')
16
+ return
17
+
18
+ delete_session()
19
+ print('[green][+][/] ログアウトしました.')
20
+
21
+ window = webview.create_window('AtCoder Logout', ATCODER_LOGIN_URL, hidden=True)
22
+
23
+ def on_start():
24
+ window.clear_cookies()
25
+ window.destroy()
26
+
27
+ webview.start(on_start, private_mode=False)
atcdr/markdown.py CHANGED
@@ -1,39 +1,43 @@
1
1
  import os
2
2
 
3
+ import rich_click as click
3
4
  from rich.console import Console
4
5
  from rich.markdown import Markdown
5
6
 
6
- from atcdr.util.execute import execute_files
7
+ from atcdr.util.fileops import add_file_selector
7
8
  from atcdr.util.filetype import FILE_EXTENSIONS, Lang
8
- from atcdr.util.problem import make_problem_markdown
9
+ from atcdr.util.parse import ProblemHTML
9
10
 
10
11
 
11
12
  def save_markdown(html_path: str, lang: str) -> None:
12
- console = Console()
13
- with open(html_path, 'r', encoding='utf-8') as f:
14
- html = f.read()
15
- md = make_problem_markdown(html, lang)
16
- file_without_ext = os.path.splitext(html_path)[0]
17
- md_path = file_without_ext + FILE_EXTENSIONS[Lang.MARKDOWN]
18
-
19
- with open(md_path, 'w', encoding='utf-8') as f:
20
- f.write(md)
21
- console.print('[green][+][/green] Markdownファイルを作成しました.')
22
-
23
-
24
- def print_markdown(md_path: str) -> None:
25
- console = Console()
26
- with open(md_path, 'r', encoding='utf-8') as f:
27
- md = f.read()
28
- console.print(Markdown(md))
29
-
30
-
31
- def markdown(*args: str, lang: str = 'ja', save: bool = False) -> None:
32
- if save:
33
- execute_files(
34
- *args,
35
- func=lambda html_path: save_markdown(html_path, lang),
36
- target_filetypes=[Lang.HTML],
37
- )
38
- else:
39
- execute_files(*args, func=print_markdown, target_filetypes=[Lang.MARKDOWN])
13
+ console = Console()
14
+ with open(html_path, 'r', encoding='utf-8') as f:
15
+ html = ProblemHTML(f.read())
16
+ md = html.make_problem_markdown(lang)
17
+ file_without_ext = os.path.splitext(html_path)[0]
18
+ md_path = file_without_ext + FILE_EXTENSIONS[Lang.MARKDOWN]
19
+
20
+ with open(md_path, 'w', encoding='utf-8') as f:
21
+ f.write(md)
22
+ console.print('[green][+][/green] Markdownファイルを作成しました.')
23
+
24
+
25
+ def print_markdown(html_path: str, lang: str) -> None:
26
+ console = Console()
27
+ with open(html_path, 'r', encoding='utf-8') as f:
28
+ html = ProblemHTML(f.read())
29
+ md = html.make_problem_markdown(lang)
30
+ console.print(Markdown(md))
31
+
32
+
33
+ @click.command(short_help='Markdown形式で問題を表示します')
34
+ @add_file_selector('files', filetypes=[Lang.HTML])
35
+ @click.option('--lang', default='ja', help='出力する言語を指定')
36
+ @click.option('--save', is_flag=True, help='変換結果をファイルに保存')
37
+ def markdown(files, lang, save):
38
+ """Markdown形式で問題を表示します"""
39
+ for path in files:
40
+ if save:
41
+ save_markdown(path, lang)
42
+ else:
43
+ print_markdown(path, lang)