SenY commited on
Commit
f9fcf2d
·
1 Parent(s): d4af610
Files changed (1) hide show
  1. index.html +406 -19
index.html CHANGED
@@ -1,19 +1,406 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ja" data-bs-theme="dark">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>テキスト編集ユーティリティ</title>
8
+ <!-- Bootstrap 5 CSS -->
9
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
10
+ <!-- Font Awesome -->
11
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
12
+ <style>
13
+ .text-area-container {
14
+ margin: 20px 0;
15
+ position: relative;
16
+ }
17
+
18
+ .text-area-controls {
19
+ position: absolute;
20
+ top: 10px;
21
+ right: 10px;
22
+ z-index: 10;
23
+ display: flex;
24
+ gap: 5px;
25
+ }
26
+
27
+ .text-area-controls .btn {
28
+ padding: 4px 8px;
29
+ font-size: 12px;
30
+ border-radius: 4px;
31
+ }
32
+
33
+ .text-area-container textarea {
34
+ padding-right: 80px;
35
+ }
36
+
37
+ .accordion-button:not(.collapsed) {
38
+ background-color: var(--bs-primary-bg-subtle);
39
+ }
40
+
41
+ .layout-wrapper {
42
+ display: flex;
43
+ flex-direction: row;
44
+ height: 100vh;
45
+ }
46
+
47
+ .sidebar {
48
+ width: 250px;
49
+ min-width: 250px;
50
+ transition: all 0.3s;
51
+ z-index: 1000;
52
+ background-color: var(--bs-body-bg);
53
+ border-right: 1px solid var(--bs-border-color, #444);
54
+ margin-top: 32px;
55
+ }
56
+
57
+ .main-content {
58
+ flex: 1 1 0%;
59
+ transition: all 0.3s;
60
+ margin-top: 32px;
61
+ display: flex;
62
+ flex-direction: column;
63
+ align-items: stretch;
64
+ }
65
+
66
+ .main-inner {
67
+ width: 100%;
68
+ max-width: 100%;
69
+ padding: 0 8px;
70
+ margin: 0;
71
+ }
72
+
73
+ @media (min-width: 768px) {
74
+ .layout-wrapper {
75
+ padding: 0 25vh;
76
+ }
77
+ }
78
+ </style>
79
+ </head>
80
+
81
+ <body>
82
+ <div class="layout-wrapper">
83
+
84
+ <!-- メインコンテンツ -->
85
+ <div class="main-content" id="mainContent">
86
+ <div class="main-inner">
87
+ <h2 class="mb-3">テキスト編集ユーティリティ</h2>
88
+ <div class="d-grid gap-2">
89
+ <button class="btn btn-primary" id="processBtn">
90
+ <i class="fas fa-cog me-2"></i>Process
91
+ </button>
92
+ <button class="btn btn-secondary" id="deprocessBtn">
93
+ <i class="fas fa-undo me-2"></i>Deprocess
94
+ </button>
95
+ </div>
96
+ <div class="accordion" id="textEditorAccordion">
97
+ <!-- 上部テキストエリア -->
98
+ <div class="accordion-item">
99
+ <h2 class="accordion-header">
100
+ <button class="accordion-button" type="button" data-bs-toggle="collapse"
101
+ data-bs-target="#collapseOne">
102
+ <i class="fas fa-chevron-down me-2"></i>上部テキストエリア
103
+ </button>
104
+ </h2>
105
+ <div id="collapseOne" class="accordion-collapse collapse show"
106
+ data-bs-parent="#textEditorAccordion">
107
+ <div class="accordion-body">
108
+ <div class="text-area-container">
109
+ <div class="text-area-controls">
110
+ <button class="btn btn-outline-primary btn-sm" onclick="copyToClipboard('topText', event)" title="コピー">
111
+ <i class="fas fa-copy"></i>
112
+ </button>
113
+ <button class="btn btn-outline-secondary btn-sm" onclick="pasteFromClipboard('topText')" title="ペースト">
114
+ <i class="fas fa-paste"></i>
115
+ </button>
116
+ </div>
117
+ <textarea id="topText" class="form-control" style="width:100%; min-height:50vh;"
118
+ rows="10" placeholder="ここにテキストを入力してく���さい"></textarea>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ <!-- 下部テキストエリア -->
124
+ <div class="accordion-item">
125
+ <h2 class="accordion-header">
126
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
127
+ data-bs-target="#collapseTwo">
128
+ <i class="fas fa-chevron-down me-2"></i>下部テキストエリア
129
+ </button>
130
+ </h2>
131
+ <div id="collapseTwo" class="accordion-collapse collapse" data-bs-parent="#textEditorAccordion">
132
+ <div class="accordion-body">
133
+ <div class="text-area-container">
134
+ <div class="text-area-controls">
135
+ <button class="btn btn-outline-primary btn-sm" onclick="copyToClipboard('bottomText', event)" title="コピー">
136
+ <i class="fas fa-copy"></i>
137
+ </button>
138
+ <button class="btn btn-outline-secondary btn-sm" onclick="pasteFromClipboard('bottomText')" title="ペースト">
139
+ <i class="fas fa-paste"></i>
140
+ </button>
141
+ </div>
142
+ <textarea id="bottomText" class="form-control" style="width:100%; min-height:50vh;"
143
+ rows="10" placeholder="ここにテキストを入力してください"></textarea>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+
149
+ <div class="accordion-item">
150
+ <h2 class="accordion-header">
151
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
152
+ data-bs-target="#collapseThree">
153
+ <i class="fas fa-chevron-down me-2"></i>メモ
154
+ </button>
155
+ </h2>
156
+ <div id="collapseThree" class="accordion-collapse collapse"
157
+ data-bs-parent="#textEditorAccordion">
158
+ <div class="accordion-body">
159
+ <div class="text-area-container">
160
+ <div class="text-area-controls">
161
+ <button class="btn btn-outline-primary btn-sm" onclick="copyToClipboard('memoArea', event)" title="コピー">
162
+ <i class="fas fa-copy"></i>
163
+ </button>
164
+ <button class="btn btn-outline-secondary btn-sm" onclick="pasteFromClipboard('memoArea')" title="ペースト">
165
+ <i class="fas fa-paste"></i>
166
+ </button>
167
+ </div>
168
+ <textarea id="memoArea" class="form-control" style="width:100%; min-height:50vh;"
169
+ rows="10" placeholder="ここにテキストを入力してください"></textarea>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+ <!-- Bootstrap 5 JS Bundle with Popper -->
181
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
182
+ <script>
183
+ let lastSaveTimestamp = 0;
184
+
185
+ // 0pxスペース(ゼロ幅スペース)の定数
186
+ const ZERO_WIDTH_SPACE = '&#8204;';
187
+
188
+ // 既知のダミー文字の候補
189
+ const KNOWN_DUMMY_CHARS = [
190
+ '\u200c', '\u200b', '\u200d', // ゼロ幅文字
191
+ '&#8204;', '&#8203;', '&zwnj;', // HTMLエンティティ
192
+ '\u200e', '\u200f', // 方向制御文字
193
+ '\u2060', '\u2061', '\u2062', '\u2063', '\u2064' // その他の制御文字
194
+ ];
195
+
196
+ // 文字の出現頻度を分析する関数
197
+ function analyzeCharFrequency(text) {
198
+ const frequency = new Map();
199
+ for (let i = 0; i < text.length; i++) {
200
+ const char = text[i];
201
+ frequency.set(char, (frequency.get(char) || 0) + 1);
202
+ }
203
+ return frequency;
204
+ }
205
+
206
+ // 文字列のパターンを分析してダミー文字の候補を探す関数
207
+ function findPatternCandidate(text) {
208
+ if (!text || text.length < 3) return null;
209
+
210
+ // 文字の出現頻度を分析
211
+ const frequency = analyzeCharFrequency(text);
212
+
213
+ // 文字列を2文字ずつに分割して、各文字の間の文字を確認
214
+ const patternMap = new Map();
215
+ for (let i = 1; i < text.length - 1; i += 2) {
216
+ const char = text[i];
217
+ const prevChar = text[i - 1];
218
+ const nextChar = text[i + 1];
219
+
220
+ // 前後の文字が同じで、かつ現在の文字が一定の頻度で出現している場合
221
+ if (prevChar === nextChar &&
222
+ frequency.get(char) > text.length * 0.3) { // 30%以上の出現率
223
+ patternMap.set(char, (patternMap.get(char) || 0) + 1);
224
+ }
225
+ }
226
+
227
+ // 最も頻出するパターンを返す
228
+ let maxCount = 0;
229
+ let candidate = null;
230
+ for (const [char, count] of patternMap) {
231
+ if (count > maxCount) {
232
+ maxCount = count;
233
+ candidate = char;
234
+ }
235
+ }
236
+
237
+ return candidate;
238
+ }
239
+
240
+ // 文字間のダミー文字を検出する関数
241
+ function detectDummyChar(text) {
242
+ if (!text || text.length < 3) return null;
243
+
244
+ // まず既知のダミー文字をチェック
245
+ for (let i = 1; i < text.length - 1; i += 2) {
246
+ const char = text[i];
247
+ if (KNOWN_DUMMY_CHARS.includes(char) &&
248
+ text[i - 1] !== char && text[i + 1] !== char) {
249
+ return char;
250
+ }
251
+ }
252
+
253
+ // 既知のダミー文字が見つからない場合はパターン分析を実行
254
+ return findPatternCandidate(text);
255
+ }
256
+
257
+ // 文字列の各文字の間に指定の文字列を挟む
258
+ function insertBetweenChars(text, insertStr) {
259
+ if (!text) return '';
260
+ return text.split('').join(insertStr);
261
+ }
262
+
263
+ // 文字列から指定の文字列を除去
264
+ function removeBetweenChars(text, removeStr) {
265
+ if (!text) return '';
266
+ return text.split(removeStr).join('');
267
+ }
268
+
269
+ // HTMLエンティティを実体参照に変換する関数
270
+ function decodeHtmlEntities(str) {
271
+ const textarea = document.createElement('textarea');
272
+ textarea.innerHTML = str;
273
+ return textarea.value;
274
+ }
275
+
276
+ // テキストエリアの値を取得・設定する関数
277
+ function getUpperText() {
278
+ return document.querySelectorAll('.text-area-container textarea')[0].value;
279
+ }
280
+ function setUpperText(val) {
281
+ document.querySelectorAll('.text-area-container textarea')[0].value = val;
282
+ }
283
+ function getLowerText() {
284
+ return document.querySelectorAll('.text-area-container textarea')[1].value;
285
+ }
286
+ function setLowerText(val) {
287
+ document.querySelectorAll('.text-area-container textarea')[1].value = val;
288
+ }
289
+
290
+ // processボタンの挙動
291
+ document.getElementById('processBtn').addEventListener('click', function () {
292
+ const upperText = getUpperText();
293
+ const processed = insertBetweenChars(upperText, ZERO_WIDTH_SPACE);
294
+ setLowerText(processed);
295
+ // 下部テキストエリアを表示
296
+ const lowerAccordion = new bootstrap.Collapse(document.getElementById('collapseTwo'), {
297
+ toggle: false
298
+ });
299
+ lowerAccordion.show();
300
+ });
301
+
302
+ // deprocessボタンの挙動
303
+ document.getElementById('deprocessBtn').addEventListener('click', function () {
304
+ let lowerText = getLowerText();
305
+ // まずHTMLエンティティを実体参照に変換
306
+ lowerText = decodeHtmlEntities(lowerText);
307
+ const dummyChar = detectDummyChar(lowerText);
308
+ if (!dummyChar) {
309
+ alert('文字間のダミー文字を検出できませんでした。');
310
+ return;
311
+ }
312
+ const deprocessed = removeBetweenChars(lowerText, dummyChar);
313
+ setUpperText(deprocessed);
314
+ // 上部テキストエリアを表示
315
+ const upperAccordion = new bootstrap.Collapse(document.getElementById('collapseOne'), {
316
+ toggle: false
317
+ });
318
+ upperAccordion.show();
319
+ });
320
+
321
+ function saveToUserStorage(force = false) {
322
+ const currentTime = Date.now();
323
+ if (currentTime - lastSaveTimestamp < 5000 && !force) {
324
+ console.debug('セーブをスキップします');
325
+ return;
326
+ }
327
+ console.debug('セーブを実行します');
328
+
329
+ // 既存のデータを取得
330
+ const textUtilData = JSON.parse(localStorage.getItem('textUtil') || '{}');
331
+
332
+ const newData = {};
333
+ Array.from(document.querySelectorAll("input[id], textarea[id], select[id]")).forEach(el => {
334
+ if (el.id) {
335
+ newData[el.id] = el.type === 'checkbox' ? el.checked : el.value;
336
+ }
337
+ });
338
+ Object.assign(textUtilData, newData);
339
+ console.log(textUtilData);
340
+ localStorage.setItem('textUtil', JSON.stringify(textUtilData));
341
+ lastSaveTimestamp = currentTime;
342
+ }
343
+
344
+ function loadFromUserStorage() {
345
+ const textUtilData = JSON.parse(localStorage.getItem('textUtil') || '{}');
346
+ document.getElementById('bottomText').value = textUtilData['bottomText'] || '';
347
+ document.getElementById('topText').value = textUtilData['topText'] || '';
348
+ document.getElementById('memoArea').value = textUtilData['memoArea'] || '';
349
+ }
350
+
351
+ document.querySelectorAll("#bottomText, #topText").forEach(el => {
352
+ el.addEventListener('input', () => {
353
+ saveToUserStorage(false);
354
+ });
355
+ });
356
+ document.querySelectorAll("#memoArea").forEach(el => {
357
+ el.addEventListener('input', () => {
358
+ saveToUserStorage(true);
359
+ });
360
+ });
361
+
362
+ document.addEventListener('DOMContentLoaded', function () {
363
+ // ページ読み込み時にデータを復元
364
+ loadFromUserStorage();
365
+ });
366
+
367
+ // クリップボードにコピーする関数
368
+ async function copyToClipboard(textareaId, event) {
369
+ const textarea = document.getElementById(textareaId);
370
+ const text = textarea.value;
371
+
372
+ try {
373
+ await navigator.clipboard.writeText(text);
374
+ // 成功時のフィードバック(オプション)
375
+ const button = event.target.closest('button');
376
+ const originalText = button.innerHTML;
377
+ button.innerHTML = '<i class="fas fa-check"></i>';
378
+ setTimeout(() => {
379
+ button.innerHTML = originalText;
380
+ }, 1000);
381
+ } catch (err) {
382
+ console.error('クリップボードへのコピーに失敗しました:', err);
383
+ alert('クリップボードへのコピーに失敗しました');
384
+ }
385
+ }
386
+
387
+ // クリップボードからペーストする関数
388
+ async function pasteFromClipboard(textareaId) {
389
+ const textarea = document.getElementById(textareaId);
390
+
391
+ try {
392
+ const text = await navigator.clipboard.readText();
393
+ textarea.value = text;
394
+ // ペースト後に自動保存
395
+ saveToUserStorage(true);
396
+ } catch (err) {
397
+ console.error('クリップボードからのペーストに失敗しました:', err);
398
+ alert('クリップボードからのペーストに失敗しました');
399
+ }
400
+ }
401
+
402
+
403
+ </script>
404
+ </body>
405
+
406
+ </html>