1
|
|
|
#!/usr/bin/env python3 |
2
|
|
|
import re |
3
|
|
|
import textwrap |
4
|
|
|
import threading |
5
|
|
|
import itertools |
6
|
|
|
import webbrowser |
7
|
|
|
import tkinter as tk |
8
|
|
|
import tkinter.ttk as ttk |
9
|
|
|
import tkinter.messagebox as messagebox |
10
|
|
|
import xml.etree.ElementTree as ET |
11
|
|
|
import json |
12
|
|
|
from urllib import request |
13
|
|
|
|
14
|
|
|
import jaconv |
15
|
|
|
import pyttsx4 |
16
|
|
|
|
17
|
|
|
from . import Definition |
18
|
|
|
|
19
|
|
|
|
20
|
|
|
class ProcessingMenuClass(Definition.DefinitionClass): |
21
|
|
|
"""処理メニューバーのクラス. |
22
|
|
|
|
23
|
|
|
・処理メニューバーにあるプログラム群 |
24
|
|
|
|
25
|
|
|
Args: |
26
|
|
|
app (instance): MainProcessingClass のインスタンス |
27
|
|
|
tokenizer (instance): Tokenizer のインスタンス |
28
|
|
|
wiki_wiki (instance): wikipediaapi.Wikipedia のインスタンス |
29
|
|
|
locale_var (str): ロケーション |
30
|
|
|
master (instance): toplevel のインスタンス |
31
|
|
|
""" |
32
|
|
|
font_size = 0 |
33
|
|
|
"""フォントのサイズ.""" |
34
|
|
|
yahoo_appid = "" |
35
|
|
|
"""Yahoo! Client ID""" |
36
|
|
|
|
37
|
|
|
def __init__(self, app, tokenizer, wiki_wiki, locale_var, master=None): |
38
|
|
|
super().__init__(locale_var, master) |
39
|
|
|
# yahooの校正支援 |
40
|
|
|
self.KOUSEI = "{urn:yahoo:jp:jlp:KouseiService}" |
41
|
|
|
self.app = app |
42
|
|
|
self.tokenizer = tokenizer |
43
|
|
|
self.wiki_wiki = wiki_wiki |
44
|
|
|
|
45
|
|
|
def ruby_huri(self): |
46
|
|
|
"""ルビをふり. |
47
|
|
|
|
48
|
|
|
・選択文字列に小説家になろうのルビを振る。 |
49
|
|
|
""" |
50
|
|
|
hon = "" |
51
|
|
|
# 選択文字列を切り取る |
52
|
|
|
set_ruby = self.app.NovelEditor.get('sel.first', 'sel.last') |
53
|
|
|
# 選択文字列を削除する |
54
|
|
|
self.app.NovelEditor.delete('sel.first', 'sel.last') |
55
|
|
|
# 形態素解析を行う |
56
|
|
|
for token in self.tokenizer.tokenize(set_ruby): |
57
|
|
|
# ルビの取得 |
58
|
|
|
ruby = "" |
59
|
|
|
ruby = jaconv.kata2hira(token.reading) |
60
|
|
|
# 解析している文字のひらがなの部分を取得 |
61
|
|
|
hira = "" |
62
|
|
|
for i in token.surface: |
63
|
|
|
if self.is_hiragana(i): |
64
|
|
|
hira += i |
65
|
|
|
# ルビがないときと、記号の時の処理 |
66
|
|
|
if ruby.replace( |
67
|
|
|
hira, '' |
68
|
|
|
) == "" or token.part_of_speech.split( |
69
|
|
|
"," |
70
|
|
|
)[0] == self.dic.get_dict("symbol"): |
71
|
|
|
hon += token.surface |
72
|
|
|
else: |
73
|
|
|
# ルビ振りを行う |
74
|
|
|
hon += "|{0}≪{1}≫{2}".format( |
75
|
|
|
token.surface.replace(hira, ''), |
76
|
|
|
ruby.replace(hira, ''), |
77
|
|
|
hira |
78
|
|
|
) |
79
|
|
|
|
80
|
|
|
# テキストを表示する |
81
|
|
|
self.app.NovelEditor.insert('insert', hon) |
82
|
|
|
|
83
|
|
|
@staticmethod |
84
|
|
|
def is_hiragana(char): |
85
|
|
|
"""文字がひらがなか判断. |
86
|
|
|
|
87
|
|
|
・与えられた文字がひらがなかどうか判断する。 |
88
|
|
|
|
89
|
|
|
Args: |
90
|
|
|
char (str): 判断する文字 |
91
|
|
|
|
92
|
|
|
Returns: |
93
|
|
|
bool: ひらがなならTrue、違うならFalse |
94
|
|
|
""" |
95
|
|
|
return (0x3040 < ord(char) < 0x3097) |
96
|
|
|
|
97
|
|
|
def count_moji(self): |
98
|
|
|
"""文字数と行数を表示. |
99
|
|
|
|
100
|
|
|
・文字数と行数をカウントして表示する。 |
101
|
|
|
""" |
102
|
|
|
# 行数の取得 |
103
|
|
|
new_line = int(self.app.NovelEditor.index('end-1c').split('.')[0]) |
104
|
|
|
# 文字列の取得 |
105
|
|
|
moji = self.app.NovelEditor.get('1.0', 'end') |
106
|
|
|
# 20文字で区切ったときの行数を数える |
107
|
|
|
gen_mai = 0 |
108
|
|
|
for val in moji.splitlines(): |
109
|
|
|
gen_mai += len(textwrap.wrap(val, 20)) |
110
|
|
|
# メッセージボックスの表示 |
111
|
|
|
messagebox.showinfo( |
112
|
|
|
self.app.dic.get_dict("Number of characters etc"), |
113
|
|
|
self.app.dic.get_dict( |
114
|
|
|
"Characters : {0} Lines : {1}\n Manuscript papers : {2}" |
115
|
|
|
) |
116
|
|
|
.format( |
117
|
|
|
len(moji)-new_line, |
118
|
|
|
new_line, |
119
|
|
|
-(-gen_mai//20))) |
120
|
|
|
|
121
|
|
|
def find_wikipedia(self): |
122
|
|
|
"""意味を検索. |
123
|
|
|
|
124
|
|
|
・Wikipedia-APIライブラリを使ってWikipediaから選択文字の意味を |
125
|
|
|
検索する。 |
126
|
|
|
""" |
127
|
|
|
# wikipediaから |
128
|
|
|
select_text = self.app.NovelEditor.selection_get() |
129
|
|
|
page_py = self.wiki_wiki.page(select_text) |
130
|
|
|
# ページがあるかどうか判断 |
131
|
|
|
if page_py.exists(): |
132
|
|
|
messagebox.showinfo( |
133
|
|
|
self.app.dic.get_dict( |
134
|
|
|
"Meaning of [{0}]" |
135
|
|
|
).format(select_text), |
136
|
|
|
page_py.summary |
137
|
|
|
) |
138
|
|
|
else: |
139
|
|
|
messagebox.showwarning( |
140
|
|
|
self.app.dic.get_dict( |
141
|
|
|
"Meaning of [{0}]" |
142
|
|
|
).format(select_text), |
143
|
|
|
self.app.dic.get_dict("Can't find.") |
144
|
|
|
) |
145
|
|
|
|
146
|
|
|
def open_becoming_novelist_page(self): |
147
|
|
|
"""小説家になろうのユーザーページを開く. |
148
|
|
|
|
149
|
|
|
・インターネットブラウザで小説家になろうのユーザーページを開く。 |
150
|
|
|
""" |
151
|
|
|
webbrowser.open("https://syosetu.com/user/top/") |
152
|
|
|
|
153
|
|
|
def read_text(self): |
154
|
|
|
"""テキストを読み上げる. |
155
|
|
|
|
156
|
|
|
・pyttsx4ライブラリを使ってテキストボックスに書かれているものを読み上げる。 |
157
|
|
|
""" |
158
|
|
|
self.app.engine = pyttsx4.init() |
159
|
|
|
self.speak = Speaking(self.app.NovelEditor.get(1.0, tk.END),self.app, daemon=True) |
160
|
|
|
self.speak.start() |
161
|
|
|
|
162
|
|
|
def pyttsx4_onend(self): |
163
|
|
|
"""文章を読み終えた時の処理. |
164
|
|
|
|
165
|
|
|
・文章を読み終えたら中止ウインドウを削除する。 |
166
|
|
|
|
167
|
|
|
""" |
168
|
|
|
if self.speak: |
169
|
|
|
self.speak.stop() |
170
|
|
|
self.speak = None |
171
|
|
|
|
172
|
|
|
def yahoo(self): |
173
|
|
|
"""Yahoo! 校正支援. |
174
|
|
|
|
175
|
|
|
・Yahoo! 校正支援を呼び出し表示する。 |
176
|
|
|
|
177
|
|
|
Args: |
178
|
|
|
event (instance): tkinter.Event のインスタンス |
179
|
|
|
""" |
180
|
|
|
html = self.yahoocall( |
181
|
|
|
self.app.NovelEditor.get('1.0', 'end -1c') |
182
|
|
|
) |
183
|
|
|
if not self.yahoo_appid == "": |
184
|
|
|
self.yahooresult(html) |
185
|
|
|
self.yahoo_tree.bind("<Double-1>", self.on_double_click_yahoo) |
186
|
|
|
|
187
|
|
|
def yahoocall(self, sentence=""): |
188
|
|
|
"""yahooの校正支援を呼び出し. |
189
|
|
|
|
190
|
|
|
・Yahoo! 校正支援をClient IDを使って呼び出す。 |
191
|
|
|
|
192
|
|
|
Args: |
193
|
|
|
sentence (str): 校正をしたい文字列 |
194
|
|
|
|
195
|
|
|
Returns: |
196
|
|
|
str: 校正結果 |
197
|
|
|
""" |
198
|
|
|
if self.yahoo_appid == "": |
199
|
|
|
messagebox.showerror( |
200
|
|
|
self.app.dic.get_dict("Yahoo! Client ID"), |
201
|
|
|
self.app.dic.get_dict( |
202
|
|
|
"Yahoo! Client ID is not find." |
203
|
|
|
"\nRead Readme.html and set it again." |
204
|
|
|
) |
205
|
|
|
) |
206
|
|
|
return |
207
|
|
|
APPID = self.yahoo_appid.rstrip('\n') |
208
|
|
|
URL = "https://jlp.yahooapis.jp/KouseiService/V2/kousei" |
209
|
|
|
headers = { |
210
|
|
|
"Content-Type": "application/json", |
211
|
|
|
"User-Agent": "Yahoo AppID: {}".format(APPID), |
212
|
|
|
} |
213
|
|
|
param_dic = { |
214
|
|
|
"id": "NovelEditor-Yahoo-Kousei", |
215
|
|
|
"jsonrpc" : "2.0", |
216
|
|
|
"method" : "jlp.kouseiservice.kousei", |
217
|
|
|
"params" : { |
218
|
|
|
"q" : sentence |
219
|
|
|
} |
220
|
|
|
} |
221
|
|
|
params = json.dumps(param_dic).encode() |
222
|
|
|
req = request.Request(URL, params, headers) |
223
|
|
|
with request.urlopen(req) as res: |
224
|
|
|
body = res.read() |
225
|
|
|
return body.decode() |
226
|
|
|
|
227
|
|
|
def yahooresult(self, html): |
228
|
|
|
"""校正支援を表示する画面を制作. |
229
|
|
|
|
230
|
|
|
・校正結果を表示するダイアログを作成する。 |
231
|
|
|
|
232
|
|
|
Args: |
233
|
|
|
html (str): 校正結果 |
234
|
|
|
""" |
235
|
|
|
jsonData = json.loads(html) |
236
|
|
|
# サブウインドウの表示 |
237
|
|
|
sub_win = tk.Toplevel(self.app) |
238
|
|
|
# ツリービューの表示 |
239
|
|
|
self.yahoo_tree = ttk.Treeview(sub_win) |
240
|
|
|
self.yahoo_tree["columns"] = (1, 2, 3, 4, 5) |
241
|
|
|
# 表スタイルの設定(headingsはツリー形式ではない、通常の表形式) |
242
|
|
|
self.yahoo_tree["show"] = "headings" |
243
|
|
|
self.yahoo_tree.column(1, width=100) |
244
|
|
|
self.yahoo_tree.column(2, width=80) |
245
|
|
|
self.yahoo_tree.column(3, width=75) |
246
|
|
|
self.yahoo_tree.column(4, width=150) |
247
|
|
|
self.yahoo_tree.column(5, width=120) |
248
|
|
|
self.yahoo_tree.heading( |
249
|
|
|
1, |
250
|
|
|
text=self.dic.get_dict("Number of characters from the beginning") |
251
|
|
|
) |
252
|
|
|
self.yahoo_tree.heading( |
253
|
|
|
2, |
254
|
|
|
text=self.dic.get_dict("Number of target characters") |
255
|
|
|
) |
256
|
|
|
self.yahoo_tree.heading( |
257
|
|
|
3, |
258
|
|
|
text=self.dic.get_dict("Target notation") |
259
|
|
|
) |
260
|
|
|
self.yahoo_tree.heading( |
261
|
|
|
4, |
262
|
|
|
text=self.dic.get_dict("Paraphrase candidate string") |
263
|
|
|
) |
264
|
|
|
self.yahoo_tree.heading( |
265
|
|
|
5, |
266
|
|
|
text=self.dic.get_dict("Detailed information on the pointed out") |
267
|
|
|
) |
268
|
|
|
# 情報を取り出す |
269
|
|
|
for child in jsonData["result"]["suggestions"]: |
270
|
|
|
StartPos = (child["offset"]) |
271
|
|
|
Length = (child["length"]) |
272
|
|
|
Surface = (child["word"]) |
273
|
|
|
ShitekiWord = (child["note"]) |
274
|
|
|
ShitekiInfo = (child["rule"]) |
275
|
|
|
self.yahoo_tree.insert( |
276
|
|
|
"", |
277
|
|
|
"end", |
278
|
|
|
values=(StartPos, |
279
|
|
|
Length, |
280
|
|
|
Surface, |
281
|
|
|
ShitekiWord, |
282
|
|
|
ShitekiInfo |
283
|
|
|
) |
284
|
|
|
) |
285
|
|
|
|
286
|
|
|
self.yahoo_tree.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E, tk.W)) |
287
|
|
|
# スクロールバーを表示する |
288
|
|
|
SCRLBAR_Y = ttk.Scrollbar( |
289
|
|
|
sub_win, |
290
|
|
|
orient=tk.VERTICAL, |
291
|
|
|
command=self.yahoo_tree.yview |
292
|
|
|
) |
293
|
|
|
self.yahoo_tree.configure(yscroll=SCRLBAR_Y.set) |
294
|
|
|
SCRLBAR_Y.grid(row=0, column=1, sticky=(tk.N, tk.S)) |
295
|
|
|
# 最前面に表示し続ける |
296
|
|
|
sub_win.attributes("-topmost", True) |
297
|
|
|
sub_win.title( |
298
|
|
|
self.app.dic.get_dict("Sentence structure") |
299
|
|
|
) |
300
|
|
|
|
301
|
|
|
def on_double_click_yahoo(self, event=None): |
302
|
|
|
"""Yahoo! 校正支援リストをダブルクリック. |
303
|
|
|
|
304
|
|
|
・Yahoo! 校正支援ダイアログのリストをダブルクリックすると |
305
|
|
|
その該当箇所を選択する。 |
306
|
|
|
|
307
|
|
|
Args: |
308
|
|
|
event (instance): tkinter.Event のインスタンス |
309
|
|
|
""" |
310
|
|
|
i = 0 |
311
|
|
|
textlen = 0 |
312
|
|
|
textforlen = 0 |
313
|
|
|
curItem = self.yahoo_tree.focus() |
314
|
|
|
value = self.yahoo_tree.item(curItem) |
315
|
|
|
# 出てくる場所を取得 |
316
|
|
|
val = int(value.get("values")[0]) |
317
|
|
|
# 出てくる文字数を取得 |
318
|
|
|
lenge = value.get("values")[1] |
319
|
|
|
# 何行目になるか確認する |
320
|
|
|
while True: |
321
|
|
|
if val > textlen: |
322
|
|
|
i += 1 |
323
|
|
|
textforlen = textlen |
324
|
|
|
textlen += len( |
325
|
|
|
self.app.NovelEditor.get( |
326
|
|
|
'{0}.0'.format(i), |
327
|
|
|
'{0}.0'.format(i+1) |
328
|
|
|
) |
329
|
|
|
) |
330
|
|
|
else: |
331
|
|
|
break |
332
|
|
|
if i == 0: |
333
|
|
|
i = 1 |
334
|
|
|
# 選択状態を一旦削除 |
335
|
|
|
self.app.NovelEditor.tag_remove('sel', '1.0', 'end') |
336
|
|
|
# 選択状態にする |
337
|
|
|
self.app.NovelEditor.tag_add( |
338
|
|
|
'sel', |
339
|
|
|
"{0}.{1}".format(i, val-textforlen), |
340
|
|
|
"{0}.{1}".format(i, val-textforlen+lenge) |
341
|
|
|
) |
342
|
|
|
# カーソルの移動 |
343
|
|
|
self.app.NovelEditor.mark_set('insert', '{0}.{1}'.format(i, val-textforlen)) |
344
|
|
|
self.app.NovelEditor.see('insert') |
345
|
|
|
# フォーカスを合わせる |
346
|
|
|
self.app.NovelEditor.focus() |
347
|
|
|
return |
348
|
|
|
|
349
|
|
|
def font_dialog(self, event=None): |
350
|
|
|
"""フォントサイズダイアログを作成. |
351
|
|
|
|
352
|
|
|
・フォントサイズダイアログを作成し表示する。 |
353
|
|
|
|
354
|
|
|
Args: |
355
|
|
|
event (instance): tkinter.Event のインスタンス |
356
|
|
|
""" |
357
|
|
|
self.app.sub_wins = tk.Toplevel(self.app) |
358
|
|
|
self.app.intSpin = ttk.Spinbox(self.app.sub_wins, from_=12, to=72) |
359
|
|
|
self.app.intSpin.grid( |
360
|
|
|
row=0, |
361
|
|
|
column=0, |
362
|
|
|
columnspan=2, |
363
|
|
|
padx=5, |
364
|
|
|
pady=5, |
365
|
|
|
sticky=tk.W+tk.E, |
366
|
|
|
ipady=3 |
367
|
|
|
) |
368
|
|
|
button = ttk.Button( |
369
|
|
|
self.app.sub_wins, |
370
|
|
|
text=self.app.dic.get_dict("Resize"), |
371
|
|
|
width=str(self.app.dic.get_dict("Resize")), |
372
|
|
|
padding=(10, 5), |
373
|
|
|
command=self.font_size_Change |
374
|
|
|
) |
375
|
|
|
button.grid(row=1, column=1) |
376
|
|
|
self.app.intSpin.set(ProcessingMenuClass.font_size) |
377
|
|
|
self.app.sub_wins.title( |
378
|
|
|
self.app.dic.get_dict("Font size") |
379
|
|
|
) |
380
|
|
|
|
381
|
|
|
def font_size_Change(self): |
382
|
|
|
"""フォントのサイズの変更. |
383
|
|
|
|
384
|
|
|
・サイズ変更を押されたときにサイズを変更する。 |
385
|
|
|
上は72ptまで下は12ptまでにする。 |
386
|
|
|
""" |
387
|
|
|
# 比較のため数値列に変更 |
388
|
|
|
font_size = int(self.app.intSpin.get()) |
389
|
|
|
if font_size < 12: # 12より下の値を入力した時、12にする |
390
|
|
|
font_size = 12 |
391
|
|
|
elif 72 < font_size: # 72より上の値を入力した時、72にする |
392
|
|
|
font_size = 72 |
393
|
|
|
# 文字列にもどす |
394
|
|
|
self.font_size_input(str(font_size)) |
395
|
|
|
self.app.sub_wins.destroy() |
396
|
|
|
# フォントサイズの変更 |
397
|
|
|
self.app.NovelEditor.configure(font=(self.app.font, self.font_size)) |
398
|
|
|
# ラインナンバーの変更 |
399
|
|
|
self.app.spc.update_line_numbers() |
400
|
|
|
# ハイライトのやり直し |
401
|
|
|
self.app.hpc.all_highlight() |
402
|
|
|
|
403
|
|
|
@classmethod |
404
|
|
|
def font_size_input(cls, font_size): |
405
|
|
|
"""フォントサイズを入力. |
406
|
|
|
|
407
|
|
|
・フォントサイズをクラス変数に入力する。 |
408
|
|
|
|
409
|
|
|
Args: |
410
|
|
|
font_size (str): フォントサイズ |
411
|
|
|
""" |
412
|
|
|
cls.font_size = font_size |
413
|
|
|
|
414
|
|
|
|
415
|
|
|
class Speaking(threading.Thread): |
416
|
|
|
"""テキスト読み上げのクラス. |
417
|
|
|
|
418
|
|
|
・テキスト読み上げのプログラム群 |
419
|
|
|
|
420
|
|
|
Args: |
421
|
|
|
sentence (str): テキストデータ |
422
|
|
|
app (instance): MainProcessingClass のインスタンス |
423
|
|
|
**kwargs (dict): 複数のキーワード引数を辞書として受け取る |
424
|
|
|
""" |
425
|
|
|
def __init__(self, sentence, app, **kwargs): |
426
|
|
|
super().__init__(**kwargs) |
427
|
|
|
self.app = app |
428
|
|
|
# 改行と点丸で分割(行ごとの二次元配列) |
429
|
|
|
self.words = [re.split("、|。", text) for text in sentence.splitlines()] |
430
|
|
|
# 二次元配列を一次元配列に変換 |
431
|
|
|
self.words = list(itertools.chain.from_iterable(self.words)) |
432
|
|
|
self.paused = False |
433
|
|
|
|
434
|
|
|
def run(self): |
435
|
|
|
"""読み上げを実行. |
436
|
|
|
|
437
|
|
|
・テキストの読み上げを始める。 |
438
|
|
|
|
439
|
|
|
""" |
440
|
|
|
self.running = True |
441
|
|
|
# 読むのを中止するウインドウを作成する |
442
|
|
|
self.sub_read_win = tk.Toplevel(self.app) |
443
|
|
|
button = ttk.Button( |
444
|
|
|
self.sub_read_win, |
445
|
|
|
text=self.app.dic.get_dict("Stop"), |
446
|
|
|
width=str(self.app.dic.get_dict("Stop")), |
447
|
|
|
padding=(100, 5), |
448
|
|
|
command=self.stop |
449
|
|
|
) |
450
|
|
|
button.grid(row=1, column=1) |
451
|
|
|
# 最前面に表示し続ける |
452
|
|
|
self.sub_read_win.attributes("-topmost", True) |
453
|
|
|
# サイズ変更禁止 |
454
|
|
|
self.sub_read_win.resizable(False, False) |
455
|
|
|
self.sub_read_win.title( |
456
|
|
|
self.app.dic.get_dict("Read aloud") |
457
|
|
|
) |
458
|
|
|
# 頭から読み上げる |
459
|
|
|
pos = [0,0] |
460
|
|
|
while self.words and self.running: |
461
|
|
|
if not self.paused: |
462
|
|
|
# 読み上げ場所の選択 |
463
|
|
|
word = self.words.pop(0) |
464
|
|
|
pos[1] = len(word) + 1 + pos[1] |
465
|
|
|
if pos[1]-pos[0]>1: |
466
|
|
|
start = '0.0 + {0}c'.format(pos[0]) |
467
|
|
|
end = '0.0 + {0}c'.format(pos[1]) |
468
|
|
|
self.app.NovelEditor.tag_add('sel', start, end) |
469
|
|
|
self.app.NovelEditor.mark_set('insert', end) |
470
|
|
|
self.app.NovelEditor.see('insert') |
471
|
|
|
self.app.NovelEditor.focus() |
472
|
|
|
# 読み上げ開始 |
473
|
|
|
self.app.engine.say(word) |
474
|
|
|
self.app.engine.runAndWait() |
475
|
|
|
self.app.NovelEditor.tag_remove('sel', '1.0', 'end') |
476
|
|
|
# 読み上げ終了 |
477
|
|
|
pos[0] = pos[1] |
478
|
|
|
self.running = False |
479
|
|
|
self.sub_read_win.destroy() |
480
|
|
|
self.app.NovelEditor.tag_remove('sel', '1.0', 'end') |
481
|
|
|
|
482
|
|
|
def stop(self): |
483
|
|
|
"""読み上げを終了する. |
484
|
|
|
|
485
|
|
|
・テキストの読み上げを終わる。 |
486
|
|
|
|
487
|
|
|
""" |
488
|
|
|
self.running = False |
489
|
|
|
self.sub_read_win.destroy() |
490
|
|
|
self.app.NovelEditor.tag_remove('sel', '1.0', 'end') |
491
|
|
|
|