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/cli.py +86 -0
- atcdr/download.py +275 -242
- atcdr/generate.py +180 -198
- atcdr/login.py +136 -0
- atcdr/logout.py +27 -0
- atcdr/markdown.py +34 -30
- atcdr/open.py +37 -32
- atcdr/submit.py +302 -0
- atcdr/test.py +365 -370
- atcdr/util/fileops.py +102 -0
- atcdr/util/filetype.py +71 -71
- atcdr/util/gpt.py +102 -96
- atcdr/util/parse.py +206 -0
- atcdr/util/problem.py +94 -91
- atcdr/util/session.py +140 -0
- atcoderstudybooster-0.3.2.dist-info/METADATA +213 -0
- atcoderstudybooster-0.3.2.dist-info/RECORD +21 -0
- {atcoderstudybooster-0.3.dist-info → atcoderstudybooster-0.3.2.dist-info}/WHEEL +1 -1
- atcoderstudybooster-0.3.2.dist-info/entry_points.txt +2 -0
- atcdr/main.py +0 -40
- atcdr/util/cost.py +0 -120
- atcdr/util/execute.py +0 -63
- atcoderstudybooster-0.3.dist-info/METADATA +0 -96
- atcoderstudybooster-0.3.dist-info/RECORD +0 -17
- atcoderstudybooster-0.3.dist-info/entry_points.txt +0 -2
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
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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.
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
def render_result_for_GPT(
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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.
|
7
|
+
from atcdr.util.fileops import add_file_selector
|
7
8
|
from atcdr.util.filetype import FILE_EXTENSIONS, Lang
|
8
|
-
from atcdr.util.
|
9
|
+
from atcdr.util.parse import ProblemHTML
|
9
10
|
|
10
11
|
|
11
12
|
def save_markdown(html_path: str, lang: str) -> None:
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
def print_markdown(
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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)
|