xsl 0.1.5__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.
- xsl/__init__.py +1 -0
- xsl/cli.py +1237 -0
- xsl/editor.py +5 -0
- xsl/server.py +5 -0
- xsl/utils.py +5 -0
- xsl-0.1.5.dist-info/LICENSE +201 -0
- xsl-0.1.5.dist-info/METADATA +110 -0
- xsl-0.1.5.dist-info/RECORD +10 -0
- xsl-0.1.5.dist-info/WHEEL +4 -0
- xsl-0.1.5.dist-info/entry_points.txt +4 -0
xsl/cli.py
ADDED
@@ -0,0 +1,1237 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Universal File Editor CLI Tool
|
4
|
+
Edytor plików SVG/HTML/XML z obsługą XPath i CSS selectors
|
5
|
+
Obsługuje tryb CLI oraz serwer HTTP dla zdalnej edycji
|
6
|
+
"""
|
7
|
+
|
8
|
+
import argparse
|
9
|
+
import json
|
10
|
+
import os
|
11
|
+
import sys
|
12
|
+
import xml.etree.ElementTree as ET
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import Dict, List, Optional, Union
|
15
|
+
import re
|
16
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
17
|
+
from urllib.parse import urlparse, parse_qs
|
18
|
+
import urllib.parse
|
19
|
+
import logging
|
20
|
+
import base64
|
21
|
+
|
22
|
+
try:
|
23
|
+
from lxml import etree, html
|
24
|
+
|
25
|
+
LXML_AVAILABLE = True
|
26
|
+
except ImportError:
|
27
|
+
LXML_AVAILABLE = False
|
28
|
+
print("Warning: lxml not available. Installing: pip install lxml")
|
29
|
+
|
30
|
+
try:
|
31
|
+
from bs4 import BeautifulSoup
|
32
|
+
|
33
|
+
BS4_AVAILABLE = True
|
34
|
+
except ImportError:
|
35
|
+
BS4_AVAILABLE = False
|
36
|
+
print("Warning: BeautifulSoup4 not available. Installing: pip install beautifulsoup4")
|
37
|
+
|
38
|
+
try:
|
39
|
+
import requests
|
40
|
+
|
41
|
+
REQUESTS_AVAILABLE = True
|
42
|
+
except ImportError:
|
43
|
+
REQUESTS_AVAILABLE = False
|
44
|
+
print("Warning: requests not available. Installing: pip install requests")
|
45
|
+
|
46
|
+
|
47
|
+
class FileEditor:
|
48
|
+
"""Główna klasa do edycji plików XML/HTML/SVG"""
|
49
|
+
|
50
|
+
def __init__(self, file_path: str):
|
51
|
+
self.file_path = Path(file_path) if not file_path.startswith('http') else file_path
|
52
|
+
self.tree = None
|
53
|
+
self.root = None
|
54
|
+
self.file_type = None
|
55
|
+
self.original_content = None
|
56
|
+
self.is_remote = file_path.startswith('http')
|
57
|
+
self._load_file()
|
58
|
+
|
59
|
+
def _load_file(self):
|
60
|
+
"""Ładuje plik i określa jego typ"""
|
61
|
+
if self.is_remote:
|
62
|
+
if not REQUESTS_AVAILABLE:
|
63
|
+
raise ImportError("requests library required for remote files")
|
64
|
+
try:
|
65
|
+
response = requests.get(self.file_path)
|
66
|
+
response.raise_for_status()
|
67
|
+
self.original_content = response.text
|
68
|
+
except Exception as e:
|
69
|
+
raise ConnectionError(f"Cannot fetch remote file: {e}")
|
70
|
+
else:
|
71
|
+
if not self.file_path.exists():
|
72
|
+
raise FileNotFoundError(f"File not found: {self.file_path}")
|
73
|
+
with open(self.file_path, 'r', encoding='utf-8') as f:
|
74
|
+
self.original_content = f.read()
|
75
|
+
|
76
|
+
# Określ typ pliku
|
77
|
+
if self.is_remote:
|
78
|
+
extension = Path(urlparse(str(self.file_path)).path).suffix.lower()
|
79
|
+
else:
|
80
|
+
extension = self.file_path.suffix.lower()
|
81
|
+
|
82
|
+
content_lower = self.original_content.lower()
|
83
|
+
|
84
|
+
if extension == '.svg' or '<svg' in content_lower:
|
85
|
+
self.file_type = 'svg'
|
86
|
+
elif extension in ['.html', '.htm'] or '<!doctype html' in content_lower or '<html' in content_lower:
|
87
|
+
self.file_type = 'html'
|
88
|
+
elif extension == '.xml' or '<?xml' in content_lower:
|
89
|
+
self.file_type = 'xml'
|
90
|
+
else:
|
91
|
+
self.file_type = 'xml' # domyślnie XML
|
92
|
+
|
93
|
+
# Parsuj plik
|
94
|
+
if LXML_AVAILABLE:
|
95
|
+
try:
|
96
|
+
if self.file_type == 'html':
|
97
|
+
self.tree = html.fromstring(self.original_content)
|
98
|
+
else:
|
99
|
+
self.tree = etree.fromstring(self.original_content.encode('utf-8'))
|
100
|
+
self.root = self.tree
|
101
|
+
except Exception as e:
|
102
|
+
print(f"lxml parsing failed: {e}, trying ElementTree...")
|
103
|
+
self._fallback_parse()
|
104
|
+
else:
|
105
|
+
self._fallback_parse()
|
106
|
+
|
107
|
+
def _fallback_parse(self):
|
108
|
+
"""Fallback parsing using ElementTree"""
|
109
|
+
try:
|
110
|
+
self.tree = ET.fromstring(self.original_content)
|
111
|
+
self.root = self.tree
|
112
|
+
except ET.ParseError as e:
|
113
|
+
raise ValueError(f"Cannot parse file: {e}")
|
114
|
+
|
115
|
+
def find_by_xpath(self, xpath: str) -> List:
|
116
|
+
"""Znajdź elementy używając XPath"""
|
117
|
+
if not LXML_AVAILABLE:
|
118
|
+
raise NotImplementedError("XPath requires lxml library")
|
119
|
+
|
120
|
+
try:
|
121
|
+
if isinstance(self.tree, etree._Element):
|
122
|
+
# Zarejestruj standardowe przestrzenie nazw
|
123
|
+
namespaces = {
|
124
|
+
'svg': 'http://www.w3.org/2000/svg',
|
125
|
+
'xlink': 'http://www.w3.org/1999/xlink',
|
126
|
+
'html': 'http://www.w3.org/1999/xhtml'
|
127
|
+
}
|
128
|
+
return self.tree.xpath(xpath, namespaces=namespaces)
|
129
|
+
else:
|
130
|
+
return []
|
131
|
+
except Exception as e:
|
132
|
+
raise ValueError(f"Invalid XPath expression: {e}")
|
133
|
+
|
134
|
+
def find_by_css(self, css_selector: str) -> List:
|
135
|
+
"""Znajdź elementy używając CSS selectors (tylko HTML)"""
|
136
|
+
if not BS4_AVAILABLE:
|
137
|
+
raise NotImplementedError("CSS selectors require beautifulsoup4 library")
|
138
|
+
|
139
|
+
if self.file_type != 'html':
|
140
|
+
raise ValueError("CSS selectors work only with HTML files")
|
141
|
+
|
142
|
+
soup = BeautifulSoup(self.original_content, 'html.parser')
|
143
|
+
return soup.select(css_selector)
|
144
|
+
|
145
|
+
def extract_data_uri(self, xpath: str) -> Dict[str, str]:
|
146
|
+
"""Wyciągnij Data URI z SVG i zdekoduj zawartość"""
|
147
|
+
elements = self.find_by_xpath(xpath)
|
148
|
+
if not elements:
|
149
|
+
return {"error": "No elements found with given XPath"}
|
150
|
+
|
151
|
+
element = elements[0]
|
152
|
+
data_uri = None
|
153
|
+
|
154
|
+
# Sprawdź różne atrybuty, które mogą zawierać Data URI
|
155
|
+
for attr in ['href', 'xlink:href', 'src', 'data']:
|
156
|
+
if hasattr(element, 'get'):
|
157
|
+
uri = element.get(attr)
|
158
|
+
elif hasattr(element, 'attrib'):
|
159
|
+
uri = element.attrib.get(attr)
|
160
|
+
else:
|
161
|
+
continue
|
162
|
+
|
163
|
+
if uri and uri.startswith('data:'):
|
164
|
+
data_uri = uri
|
165
|
+
break
|
166
|
+
|
167
|
+
if not data_uri:
|
168
|
+
return {"error": "No Data URI found in element"}
|
169
|
+
|
170
|
+
try:
|
171
|
+
# Rozdziel nagłówek i dane
|
172
|
+
header, base64_data = data_uri.split(',', 1)
|
173
|
+
mime_type = header.split(':')[1].split(';')[0]
|
174
|
+
|
175
|
+
# Dekoduj base64
|
176
|
+
decoded_data = base64.b64decode(base64_data)
|
177
|
+
|
178
|
+
return {
|
179
|
+
"mime_type": mime_type,
|
180
|
+
"size": len(decoded_data),
|
181
|
+
"base64_data": base64_data,
|
182
|
+
"decoded_size": len(decoded_data)
|
183
|
+
}
|
184
|
+
except Exception as e:
|
185
|
+
return {"error": f"Failed to decode Data URI: {e}"}
|
186
|
+
|
187
|
+
def save_data_uri_to_file(self, xpath: str, output_path: str) -> bool:
|
188
|
+
"""Zapisz zawartość Data URI do pliku"""
|
189
|
+
result = self.extract_data_uri(xpath)
|
190
|
+
if "error" in result:
|
191
|
+
print(f"Error: {result['error']}")
|
192
|
+
return False
|
193
|
+
|
194
|
+
try:
|
195
|
+
decoded_data = base64.b64decode(result['base64_data'])
|
196
|
+
with open(output_path, 'wb') as f:
|
197
|
+
f.write(decoded_data)
|
198
|
+
print(f"Data URI saved to {output_path} ({result['size']} bytes, {result['mime_type']})")
|
199
|
+
return True
|
200
|
+
except Exception as e:
|
201
|
+
print(f"Error saving file: {e}")
|
202
|
+
return False
|
203
|
+
|
204
|
+
def get_element_text(self, xpath: str) -> str:
|
205
|
+
"""Pobierz tekst elementu"""
|
206
|
+
elements = self.find_by_xpath(xpath)
|
207
|
+
if elements:
|
208
|
+
element = elements[0]
|
209
|
+
if hasattr(element, 'text'):
|
210
|
+
return element.text or ""
|
211
|
+
return str(element)
|
212
|
+
return ""
|
213
|
+
|
214
|
+
def get_element_attribute(self, xpath: str, attr_name: str) -> str:
|
215
|
+
"""Pobierz atrybut elementu"""
|
216
|
+
elements = self.find_by_xpath(xpath)
|
217
|
+
if elements:
|
218
|
+
element = elements[0]
|
219
|
+
if hasattr(element, 'get'):
|
220
|
+
return element.get(attr_name, "")
|
221
|
+
elif hasattr(element, 'attrib'):
|
222
|
+
return element.attrib.get(attr_name, "")
|
223
|
+
return ""
|
224
|
+
|
225
|
+
def set_element_text(self, xpath: str, new_text: str) -> bool:
|
226
|
+
"""Ustaw tekst elementu"""
|
227
|
+
elements = self.find_by_xpath(xpath)
|
228
|
+
if elements:
|
229
|
+
element = elements[0]
|
230
|
+
if hasattr(element, 'text'):
|
231
|
+
element.text = new_text
|
232
|
+
return True
|
233
|
+
return False
|
234
|
+
|
235
|
+
def set_element_attribute(self, xpath: str, attr_name: str, attr_value: str) -> bool:
|
236
|
+
"""Ustaw atrybut elementu"""
|
237
|
+
elements = self.find_by_xpath(xpath)
|
238
|
+
if elements:
|
239
|
+
element = elements[0]
|
240
|
+
if hasattr(element, 'set'):
|
241
|
+
element.set(attr_name, attr_value)
|
242
|
+
elif hasattr(element, 'attrib'):
|
243
|
+
element.attrib[attr_name] = attr_value
|
244
|
+
return True
|
245
|
+
return False
|
246
|
+
|
247
|
+
def add_element(self, parent_xpath: str, tag_name: str, text: str = "", attributes: Dict[str, str] = None) -> bool:
|
248
|
+
"""Dodaj nowy element"""
|
249
|
+
parents = self.find_by_xpath(parent_xpath)
|
250
|
+
if parents:
|
251
|
+
parent = parents[0]
|
252
|
+
if LXML_AVAILABLE:
|
253
|
+
new_element = etree.SubElement(parent, tag_name)
|
254
|
+
if text:
|
255
|
+
new_element.text = text
|
256
|
+
if attributes:
|
257
|
+
for key, value in attributes.items():
|
258
|
+
new_element.set(key, value)
|
259
|
+
return True
|
260
|
+
return False
|
261
|
+
|
262
|
+
def remove_element(self, xpath: str) -> bool:
|
263
|
+
"""Usuń element"""
|
264
|
+
elements = self.find_by_xpath(xpath)
|
265
|
+
if elements:
|
266
|
+
element = elements[0]
|
267
|
+
parent = element.getparent()
|
268
|
+
if parent is not None:
|
269
|
+
parent.remove(element)
|
270
|
+
return True
|
271
|
+
return False
|
272
|
+
|
273
|
+
def list_elements(self, xpath: str = "//*") -> List[Dict]:
|
274
|
+
"""Wylistuj elementy z ich ścieżkami"""
|
275
|
+
elements = self.find_by_xpath(xpath)
|
276
|
+
result = []
|
277
|
+
|
278
|
+
for i, element in enumerate(elements):
|
279
|
+
if LXML_AVAILABLE:
|
280
|
+
element_path = self.tree.getpath(element) if hasattr(self.tree, 'getpath') else f"element[{i}]"
|
281
|
+
element_info = {
|
282
|
+
'path': element_path,
|
283
|
+
'tag': element.tag,
|
284
|
+
'text': (element.text or "").strip(),
|
285
|
+
'attributes': dict(element.attrib) if hasattr(element, 'attrib') else {}
|
286
|
+
}
|
287
|
+
result.append(element_info)
|
288
|
+
|
289
|
+
return result
|
290
|
+
|
291
|
+
def save(self, output_path: str = None) -> bool:
|
292
|
+
"""Zapisz zmiany do pliku"""
|
293
|
+
if self.is_remote and not output_path:
|
294
|
+
raise ValueError("Cannot save remote file without specifying output path")
|
295
|
+
|
296
|
+
save_path = output_path or str(self.file_path)
|
297
|
+
|
298
|
+
try:
|
299
|
+
if LXML_AVAILABLE:
|
300
|
+
if self.file_type == 'html':
|
301
|
+
content = etree.tostring(self.tree, encoding='unicode', method='html', pretty_print=True)
|
302
|
+
else:
|
303
|
+
content = etree.tostring(self.tree, encoding='unicode', pretty_print=True)
|
304
|
+
if not content.startswith('<?xml'):
|
305
|
+
content = '<?xml version="1.0" encoding="UTF-8"?>\n' + content
|
306
|
+
else:
|
307
|
+
content = ET.tostring(self.tree, encoding='unicode')
|
308
|
+
|
309
|
+
with open(save_path, 'w', encoding='utf-8') as f:
|
310
|
+
f.write(content)
|
311
|
+
return True
|
312
|
+
except Exception as e:
|
313
|
+
print(f"Error saving file: {e}")
|
314
|
+
return False
|
315
|
+
|
316
|
+
def backup(self) -> str:
|
317
|
+
"""Utwórz kopię zapasową"""
|
318
|
+
if self.is_remote:
|
319
|
+
raise ValueError("Cannot create backup of remote file")
|
320
|
+
backup_path = f"{self.file_path}.backup"
|
321
|
+
with open(backup_path, 'w', encoding='utf-8') as f:
|
322
|
+
f.write(self.original_content)
|
323
|
+
return backup_path
|
324
|
+
|
325
|
+
|
326
|
+
class FileEditorServer(BaseHTTPRequestHandler):
|
327
|
+
"""HTTP Server dla zdalnej edycji plików"""
|
328
|
+
|
329
|
+
editors: Dict[str, FileEditor] = {}
|
330
|
+
|
331
|
+
def do_GET(self):
|
332
|
+
"""Obsługa żądań GET"""
|
333
|
+
parsed_url = urlparse(self.path)
|
334
|
+
path = parsed_url.path
|
335
|
+
query = parse_qs(parsed_url.query)
|
336
|
+
|
337
|
+
if path == '/':
|
338
|
+
self._serve_interface()
|
339
|
+
elif path == '/api/files':
|
340
|
+
self._list_files()
|
341
|
+
elif path.startswith('/api/file/'):
|
342
|
+
file_path = path[10:] # usuń /api/file/
|
343
|
+
self._serve_file_info(file_path)
|
344
|
+
elif path == '/api/extract':
|
345
|
+
# Endpoint do ekstrakcji Data URI z URL + XPath
|
346
|
+
self._extract_from_url(query)
|
347
|
+
else:
|
348
|
+
self._send_error(404, "Not Found")
|
349
|
+
|
350
|
+
def do_POST(self):
|
351
|
+
"""Obsługa żądań POST"""
|
352
|
+
content_length = int(self.headers['Content-Length'])
|
353
|
+
post_data = self.rfile.read(content_length).decode('utf-8')
|
354
|
+
|
355
|
+
try:
|
356
|
+
data = json.loads(post_data)
|
357
|
+
except json.JSONDecodeError:
|
358
|
+
self._send_error(400, "Invalid JSON")
|
359
|
+
return
|
360
|
+
|
361
|
+
parsed_url = urlparse(self.path)
|
362
|
+
path = parsed_url.path
|
363
|
+
|
364
|
+
if path == '/api/load':
|
365
|
+
self._load_file(data)
|
366
|
+
elif path == '/api/query':
|
367
|
+
self._query_elements(data)
|
368
|
+
elif path == '/api/update':
|
369
|
+
self._update_element(data)
|
370
|
+
elif path == '/api/save':
|
371
|
+
self._save_file(data)
|
372
|
+
elif path == '/api/extract_data_uri':
|
373
|
+
self._extract_data_uri(data)
|
374
|
+
else:
|
375
|
+
self._send_error(404, "Not Found")
|
376
|
+
|
377
|
+
def _extract_from_url(self, query):
|
378
|
+
"""Ekstrakcja Data URI z URL + XPath (GET endpoint)"""
|
379
|
+
try:
|
380
|
+
url = query.get('url', [''])[0]
|
381
|
+
xpath = query.get('xpath', [''])[0]
|
382
|
+
|
383
|
+
if not url or not xpath:
|
384
|
+
self._send_json_response({'error': 'Missing url or xpath parameter'})
|
385
|
+
return
|
386
|
+
|
387
|
+
# Załaduj plik z URL
|
388
|
+
editor = FileEditor(url)
|
389
|
+
|
390
|
+
# Wyciągnij Data URI
|
391
|
+
result = editor.extract_data_uri(xpath)
|
392
|
+
|
393
|
+
self._send_json_response(result)
|
394
|
+
|
395
|
+
except Exception as e:
|
396
|
+
self._send_json_response({'error': str(e)})
|
397
|
+
|
398
|
+
def _serve_interface(self):
|
399
|
+
"""Serwuj interfejs webowy"""
|
400
|
+
html = """
|
401
|
+
<!DOCTYPE html>
|
402
|
+
<html>
|
403
|
+
<head>
|
404
|
+
<title>File Editor Server</title>
|
405
|
+
<style>
|
406
|
+
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
|
407
|
+
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
408
|
+
.section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; background: #fafafa; }
|
409
|
+
input, textarea, select { padding: 8px; margin: 5px; width: 300px; border: 1px solid #ccc; border-radius: 4px; }
|
410
|
+
button { padding: 10px 15px; background: #007cba; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
411
|
+
button:hover { background: #005a87; }
|
412
|
+
.result { background: #f5f5f5; padding: 10px; margin: 10px 0; border-radius: 3px; font-family: monospace; white-space: pre-wrap; max-height: 300px; overflow-y: auto; }
|
413
|
+
.error { background: #ffe6e6; color: #d00; }
|
414
|
+
.success { background: #e6ffe6; color: #0a0; }
|
415
|
+
.url-extractor { background: #e6f3ff; border-left: 4px solid #007cba; }
|
416
|
+
.tabs { display: flex; border-bottom: 1px solid #ddd; }
|
417
|
+
.tab { padding: 10px 20px; cursor: pointer; border-bottom: 2px solid transparent; }
|
418
|
+
.tab.active { border-bottom-color: #007cba; background: #f0f8ff; }
|
419
|
+
.tab-content { display: none; }
|
420
|
+
.tab-content.active { display: block; }
|
421
|
+
</style>
|
422
|
+
</head>
|
423
|
+
<body>
|
424
|
+
<div class="container">
|
425
|
+
<h1>🛠️ Universal File Editor Server</h1>
|
426
|
+
<p>Edytuj pliki SVG/HTML/XML za pomocą XPath i CSS selectors</p>
|
427
|
+
|
428
|
+
<div class="tabs">
|
429
|
+
<div class="tab active" onclick="showTab('local')">Pliki lokalne</div>
|
430
|
+
<div class="tab" onclick="showTab('remote')">Ekstrakcja z URL</div>
|
431
|
+
</div>
|
432
|
+
|
433
|
+
<div id="local" class="tab-content active">
|
434
|
+
<div class="section">
|
435
|
+
<h3>1. Załaduj plik lokalny</h3>
|
436
|
+
<input type="text" id="filePath" placeholder="Ścieżka do pliku (np. ./data.svg)" value="./example.svg">
|
437
|
+
<button onclick="loadFile()">Załaduj</button>
|
438
|
+
<div id="loadResult" class="result"></div>
|
439
|
+
</div>
|
440
|
+
|
441
|
+
<div class="section">
|
442
|
+
<h3>2. Zapytania XPath/CSS</h3>
|
443
|
+
<input type="text" id="query" placeholder="XPath (np. //text[@id='title']) lub CSS (np. .my-class)">
|
444
|
+
<select id="queryType">
|
445
|
+
<option value="xpath">XPath</option>
|
446
|
+
<option value="css">CSS Selector</option>
|
447
|
+
</select>
|
448
|
+
<button onclick="queryElements()">Wykonaj</button>
|
449
|
+
<div id="queryResult" class="result"></div>
|
450
|
+
</div>
|
451
|
+
|
452
|
+
<div class="section">
|
453
|
+
<h3>3. Edycja elementów</h3>
|
454
|
+
<input type="text" id="updateXPath" placeholder="XPath elementu do edycji">
|
455
|
+
<select id="updateType">
|
456
|
+
<option value="text">Tekst</option>
|
457
|
+
<option value="attribute">Atrybut</option>
|
458
|
+
</select>
|
459
|
+
<input type="text" id="attributeName" placeholder="Nazwa atrybutu (jeśli atrybut)">
|
460
|
+
<textarea id="newValue" placeholder="Nowa wartość"></textarea>
|
461
|
+
<button onclick="updateElement()">Aktualizuj</button>
|
462
|
+
<div id="updateResult" class="result"></div>
|
463
|
+
</div>
|
464
|
+
|
465
|
+
<div class="section">
|
466
|
+
<h3>4. Zapisz zmiany</h3>
|
467
|
+
<input type="text" id="savePath" placeholder="Ścieżka zapisu (puste = nadpisz)">
|
468
|
+
<button onclick="saveFile()">Zapisz</button>
|
469
|
+
<div id="saveResult" class="result"></div>
|
470
|
+
</div>
|
471
|
+
</div>
|
472
|
+
|
473
|
+
<div id="remote" class="tab-content">
|
474
|
+
<div class="section url-extractor">
|
475
|
+
<h3>🌐 Ekstrakcja Data URI z URL</h3>
|
476
|
+
<p>Pobierz i wyciągnij Data URI z pliku SVG/XML za pomocą XPath</p>
|
477
|
+
<input type="text" id="remoteUrl" placeholder="URL pliku (np. http://localhost/file.svg)" style="width: 400px;">
|
478
|
+
<input type="text" id="remoteXPath" placeholder="XPath (np. //svg:image/@xlink:href)" style="width: 300px;">
|
479
|
+
<button onclick="extractFromUrl()">Wyciągnij</button>
|
480
|
+
<div id="extractResult" class="result"></div>
|
481
|
+
</div>
|
482
|
+
|
483
|
+
<div class="section">
|
484
|
+
<h3>Przykłady XPath dla Data URI</h3>
|
485
|
+
<ul>
|
486
|
+
<li><code>//svg:image/@xlink:href</code> - atrybut href w elemencie image</li>
|
487
|
+
<li><code>//*[contains(@href,'data:')]/@href</code> - dowolny element z data: w href</li>
|
488
|
+
<li><code>//svg:object/@data</code> - atrybut data w object</li>
|
489
|
+
<li><code>//svg:foreignObject//text()</code> - tekst w foreignObject</li>
|
490
|
+
</ul>
|
491
|
+
</div>
|
492
|
+
</div>
|
493
|
+
</div>
|
494
|
+
|
495
|
+
<script>
|
496
|
+
function showTab(tabName) {
|
497
|
+
// Ukryj wszystkie zakładki
|
498
|
+
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
|
499
|
+
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
500
|
+
|
501
|
+
// Pokaż wybraną zakładkę
|
502
|
+
document.getElementById(tabName).classList.add('active');
|
503
|
+
event.target.classList.add('active');
|
504
|
+
}
|
505
|
+
|
506
|
+
async function apiCall(endpoint, data) {
|
507
|
+
try {
|
508
|
+
const response = await fetch(endpoint, {
|
509
|
+
method: 'POST',
|
510
|
+
headers: {'Content-Type': 'application/json'},
|
511
|
+
body: JSON.stringify(data)
|
512
|
+
});
|
513
|
+
return await response.json();
|
514
|
+
} catch (error) {
|
515
|
+
return {success: false, error: error.message};
|
516
|
+
}
|
517
|
+
}
|
518
|
+
|
519
|
+
async function extractFromUrl() {
|
520
|
+
const url = document.getElementById('remoteUrl').value;
|
521
|
+
const xpath = document.getElementById('remoteXPath').value;
|
522
|
+
|
523
|
+
if (!url || !xpath) {
|
524
|
+
document.getElementById('extractResult').textContent = 'Podaj URL i XPath';
|
525
|
+
return;
|
526
|
+
}
|
527
|
+
|
528
|
+
try {
|
529
|
+
const response = await fetch(`/api/extract?url=${encodeURIComponent(url)}&xpath=${encodeURIComponent(xpath)}`);
|
530
|
+
const result = await response.json();
|
531
|
+
|
532
|
+
document.getElementById('extractResult').textContent = JSON.stringify(result, null, 2);
|
533
|
+
document.getElementById('extractResult').className = 'result ' + (result.error ? 'error' : 'success');
|
534
|
+
} catch (error) {
|
535
|
+
document.getElementById('extractResult').textContent = 'Błąd: ' + error.message;
|
536
|
+
document.getElementById('extractResult').className = 'result error';
|
537
|
+
}
|
538
|
+
}
|
539
|
+
|
540
|
+
async function loadFile() {
|
541
|
+
const filePath = document.getElementById('filePath').value;
|
542
|
+
const result = await apiCall('/api/load', {file_path: filePath});
|
543
|
+
document.getElementById('loadResult').textContent = JSON.stringify(result, null, 2);
|
544
|
+
document.getElementById('loadResult').className = 'result ' + (result.success ? 'success' : 'error');
|
545
|
+
}
|
546
|
+
|
547
|
+
async function queryElements() {
|
548
|
+
const query = document.getElementById('query').value;
|
549
|
+
const queryType = document.getElementById('queryType').value;
|
550
|
+
const result = await apiCall('/api/query', {query, type: queryType});
|
551
|
+
document.getElementById('queryResult').textContent = JSON.stringify(result, null, 2);
|
552
|
+
document.getElementById('queryResult').className = 'result ' + (result.success ? 'success' : 'error');
|
553
|
+
}
|
554
|
+
|
555
|
+
async function updateElement() {
|
556
|
+
const xpath = document.getElementById('updateXPath').value;
|
557
|
+
const updateType = document.getElementById('updateType').value;
|
558
|
+
const attributeName = document.getElementById('attributeName').value;
|
559
|
+
const newValue = document.getElementById('newValue').value;
|
560
|
+
|
561
|
+
const data = {xpath, type: updateType, value: newValue};
|
562
|
+
if (updateType === 'attribute') {
|
563
|
+
data.attribute = attributeName;
|
564
|
+
}
|
565
|
+
|
566
|
+
const result = await apiCall('/api/update', data);
|
567
|
+
document.getElementById('updateResult').textContent = JSON.stringify(result, null, 2);
|
568
|
+
document.getElementById('updateResult').className = 'result ' + (result.success ? 'success' : 'error');
|
569
|
+
}
|
570
|
+
|
571
|
+
async function saveFile() {
|
572
|
+
const savePath = document.getElementById('savePath').value;
|
573
|
+
const result = await apiCall('/api/save', {output_path: savePath || null});
|
574
|
+
document.getElementById('saveResult').textContent = JSON.stringify(result, null, 2);
|
575
|
+
document.getElementById('saveResult').className = 'result ' + (result.success ? 'success' : 'error');
|
576
|
+
}
|
577
|
+
</script>
|
578
|
+
</body>
|
579
|
+
</html>
|
580
|
+
"""
|
581
|
+
self._send_response(200, html, 'text/html')
|
582
|
+
|
583
|
+
def _load_file(self, data):
|
584
|
+
"""Załaduj plik do edycji"""
|
585
|
+
try:
|
586
|
+
file_path = data['file_path']
|
587
|
+
editor = FileEditor(file_path)
|
588
|
+
self.editors[file_path] = editor
|
589
|
+
|
590
|
+
response = {
|
591
|
+
'success': True,
|
592
|
+
'message': f'File loaded: {file_path}',
|
593
|
+
'file_type': editor.file_type,
|
594
|
+
'is_remote': editor.is_remote,
|
595
|
+
'elements_count': len(editor.find_by_xpath("//*")) if LXML_AVAILABLE else 0
|
596
|
+
}
|
597
|
+
except Exception as e:
|
598
|
+
response = {'success': False, 'error': str(e)}
|
599
|
+
|
600
|
+
self._send_json_response(response)
|
601
|
+
|
602
|
+
def _query_elements(self, data):
|
603
|
+
"""Wykonaj zapytanie XPath/CSS"""
|
604
|
+
try:
|
605
|
+
query = data['query']
|
606
|
+
query_type = data.get('type', 'xpath')
|
607
|
+
|
608
|
+
# Znajdź pierwszy załadowany plik
|
609
|
+
if not self.editors:
|
610
|
+
raise ValueError("No files loaded")
|
611
|
+
|
612
|
+
editor = list(self.editors.values())[0]
|
613
|
+
|
614
|
+
if query_type == 'xpath':
|
615
|
+
elements = editor.list_elements(query)
|
616
|
+
else:
|
617
|
+
elements = editor.find_by_css(query)
|
618
|
+
# Konwertuj wyniki CSS na format podobny do XPath
|
619
|
+
elements = [{'tag': str(elem.name), 'text': elem.get_text(), 'attributes': elem.attrs}
|
620
|
+
for elem in elements]
|
621
|
+
|
622
|
+
response = {
|
623
|
+
'success': True,
|
624
|
+
'elements': elements,
|
625
|
+
'count': len(elements)
|
626
|
+
}
|
627
|
+
except Exception as e:
|
628
|
+
response = {'success': False, 'error': str(e)}
|
629
|
+
|
630
|
+
self._send_json_response(response)
|
631
|
+
|
632
|
+
def _update_element(self, data):
|
633
|
+
"""Aktualizuj element"""
|
634
|
+
try:
|
635
|
+
xpath = data['xpath']
|
636
|
+
update_type = data['type']
|
637
|
+
value = data['value']
|
638
|
+
|
639
|
+
if not self.editors:
|
640
|
+
raise ValueError("No files loaded")
|
641
|
+
|
642
|
+
editor = list(self.editors.values())[0]
|
643
|
+
|
644
|
+
if update_type == 'text':
|
645
|
+
success = editor.set_element_text(xpath, value)
|
646
|
+
elif update_type == 'attribute':
|
647
|
+
attribute = data['attribute']
|
648
|
+
success = editor.set_element_attribute(xpath, attribute, value)
|
649
|
+
else:
|
650
|
+
raise ValueError(f"Unknown update type: {update_type}")
|
651
|
+
|
652
|
+
response = {
|
653
|
+
'success': success,
|
654
|
+
'message': f'Element updated successfully' if success else 'Element not found'
|
655
|
+
}
|
656
|
+
except Exception as e:
|
657
|
+
response = {'success': False, 'error': str(e)}
|
658
|
+
|
659
|
+
self._send_json_response(response)
|
660
|
+
|
661
|
+
def _save_file(self, data):
|
662
|
+
"""Zapisz plik"""
|
663
|
+
try:
|
664
|
+
output_path = data.get('output_path')
|
665
|
+
|
666
|
+
if not self.editors:
|
667
|
+
raise ValueError("No files loaded")
|
668
|
+
|
669
|
+
editor = list(self.editors.values())[0]
|
670
|
+
success = editor.save(output_path)
|
671
|
+
|
672
|
+
response = {
|
673
|
+
'success': success,
|
674
|
+
'message': f'File saved successfully to {output_path or editor.file_path}'
|
675
|
+
}
|
676
|
+
except Exception as e:
|
677
|
+
response = {'success': False, 'error': str(e)}
|
678
|
+
|
679
|
+
self._send_json_response(response)
|
680
|
+
|
681
|
+
def _extract_data_uri(self, data):
|
682
|
+
"""Wyciągnij Data URI z załadowanego pliku"""
|
683
|
+
try:
|
684
|
+
xpath = data['xpath']
|
685
|
+
|
686
|
+
if not self.editors:
|
687
|
+
raise ValueError("No files loaded")
|
688
|
+
|
689
|
+
editor = list(self.editors.values())[0]
|
690
|
+
result = editor.extract_data_uri(xpath)
|
691
|
+
|
692
|
+
self._send_json_response(result)
|
693
|
+
except Exception as e:
|
694
|
+
self._send_json_response({'error': str(e)})
|
695
|
+
|
696
|
+
def _send_response(self, status_code, content, content_type='text/plain'):
|
697
|
+
"""Wyślij odpowiedź HTTP"""
|
698
|
+
self.send_response(status_code)
|
699
|
+
self.send_header('Content-type', content_type)
|
700
|
+
self.send_header('Access-Control-Allow-Origin', '*')
|
701
|
+
self.end_headers()
|
702
|
+
self.wfile.write(content.encode('utf-8'))
|
703
|
+
|
704
|
+
def _send_json_response(self, data):
|
705
|
+
"""Wyślij odpowiedź JSON"""
|
706
|
+
self._send_response(200, json.dumps(data, indent=2), 'application/json')
|
707
|
+
|
708
|
+
def _send_error(self, status_code, message):
|
709
|
+
"""Wyślij błąd"""
|
710
|
+
self._send_response(status_code, json.dumps({'error': message}), 'application/json')
|
711
|
+
|
712
|
+
|
713
|
+
class CLI:
|
714
|
+
"""Interfejs CLI"""
|
715
|
+
|
716
|
+
def __init__(self):
|
717
|
+
self.editor = None
|
718
|
+
|
719
|
+
def run(self):
|
720
|
+
"""Uruchom CLI"""
|
721
|
+
parser = argparse.ArgumentParser(description='Universal File Editor CLI')
|
722
|
+
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
723
|
+
|
724
|
+
# Load command
|
725
|
+
load_parser = subparsers.add_parser('load', help='Load file (local or remote)')
|
726
|
+
load_parser.add_argument('file', help='File path or URL')
|
727
|
+
|
728
|
+
# Query command
|
729
|
+
query_parser = subparsers.add_parser('query', help='Query elements')
|
730
|
+
query_parser.add_argument('xpath', help='XPath expression')
|
731
|
+
query_parser.add_argument('--type', choices=['text', 'attribute'], default='text')
|
732
|
+
query_parser.add_argument('--attr', help='Attribute name')
|
733
|
+
|
734
|
+
# Set command
|
735
|
+
set_parser = subparsers.add_parser('set', help='Set element value')
|
736
|
+
set_parser.add_argument('xpath', help='XPath expression')
|
737
|
+
set_parser.add_argument('value', help='New value')
|
738
|
+
set_parser.add_argument('--type', choices=['text', 'attribute'], default='text')
|
739
|
+
set_parser.add_argument('--attr', help='Attribute name')
|
740
|
+
|
741
|
+
# Extract Data URI command
|
742
|
+
extract_parser = subparsers.add_parser('extract', help='Extract Data URI from element')
|
743
|
+
extract_parser.add_argument('xpath', help='XPath to element with Data URI')
|
744
|
+
extract_parser.add_argument('--output', help='Save extracted data to file')
|
745
|
+
extract_parser.add_argument('--info', action='store_true', help='Show Data URI info only')
|
746
|
+
|
747
|
+
# List command
|
748
|
+
list_parser = subparsers.add_parser('list', help='List elements')
|
749
|
+
list_parser.add_argument('--xpath', default='//*', help='XPath filter')
|
750
|
+
list_parser.add_argument('--limit', type=int, default=20, help='Limit results')
|
751
|
+
|
752
|
+
# Save command
|
753
|
+
save_parser = subparsers.add_parser('save', help='Save file')
|
754
|
+
save_parser.add_argument('--output', help='Output file path')
|
755
|
+
|
756
|
+
# Server command
|
757
|
+
server_parser = subparsers.add_parser('server', help='Start HTTP server')
|
758
|
+
server_parser.add_argument('--port', type=int, default=8080, help='Server port')
|
759
|
+
server_parser.add_argument('--host', default='localhost', help='Server host')
|
760
|
+
|
761
|
+
# Shell command
|
762
|
+
shell_parser = subparsers.add_parser('shell', help='Interactive shell')
|
763
|
+
|
764
|
+
# Create examples command
|
765
|
+
examples_parser = subparsers.add_parser('examples', help='Create example files')
|
766
|
+
examples_parser.add_argument('--dir', default='.', help='Directory to create examples')
|
767
|
+
|
768
|
+
args = parser.parse_args()
|
769
|
+
|
770
|
+
if args.command == 'server':
|
771
|
+
self.start_server(args.host, args.port)
|
772
|
+
elif args.command == 'shell':
|
773
|
+
self.start_shell()
|
774
|
+
elif args.command == 'examples':
|
775
|
+
self.create_examples(args.dir)
|
776
|
+
else:
|
777
|
+
self.execute_command(args)
|
778
|
+
|
779
|
+
def execute_command(self, args):
|
780
|
+
"""Wykonaj komendę CLI"""
|
781
|
+
try:
|
782
|
+
if args.command == 'load':
|
783
|
+
self.editor = FileEditor(args.file)
|
784
|
+
file_type = "remote" if args.file.startswith('http') else "local"
|
785
|
+
print(f"✅ Loaded {file_type} file: {args.file} ({self.editor.file_type})")
|
786
|
+
if LXML_AVAILABLE:
|
787
|
+
elements_count = len(self.editor.find_by_xpath("//*"))
|
788
|
+
print(f" Found {elements_count} elements")
|
789
|
+
|
790
|
+
elif args.command == 'query':
|
791
|
+
if not self.editor:
|
792
|
+
print("❌ No file loaded. Use 'load' command first.")
|
793
|
+
return
|
794
|
+
|
795
|
+
if args.type == 'text':
|
796
|
+
result = self.editor.get_element_text(args.xpath)
|
797
|
+
print(f"Text: {result}")
|
798
|
+
elif args.type == 'attribute':
|
799
|
+
if not args.attr:
|
800
|
+
print("❌ --attr required for attribute queries")
|
801
|
+
return
|
802
|
+
result = self.editor.get_element_attribute(args.xpath, args.attr)
|
803
|
+
print(f"Attribute {args.attr}: {result}")
|
804
|
+
|
805
|
+
elif args.command == 'extract':
|
806
|
+
if not self.editor:
|
807
|
+
print("❌ No file loaded. Use 'load' command first.")
|
808
|
+
return
|
809
|
+
|
810
|
+
if args.info:
|
811
|
+
# Tylko informacje o Data URI
|
812
|
+
result = self.editor.extract_data_uri(args.xpath)
|
813
|
+
if "error" in result:
|
814
|
+
print(f"❌ {result['error']}")
|
815
|
+
else:
|
816
|
+
print(f"✅ Data URI found:")
|
817
|
+
print(f" MIME type: {result['mime_type']}")
|
818
|
+
print(f" Size: {result['size']} bytes")
|
819
|
+
print(f" Base64 length: {len(result['base64_data'])} chars")
|
820
|
+
elif args.output:
|
821
|
+
# Zapisz do pliku
|
822
|
+
success = self.editor.save_data_uri_to_file(args.xpath, args.output)
|
823
|
+
if not success:
|
824
|
+
print("❌ Failed to extract and save Data URI")
|
825
|
+
else:
|
826
|
+
# Wyświetl surowe dane
|
827
|
+
result = self.editor.extract_data_uri(args.xpath)
|
828
|
+
if "error" in result:
|
829
|
+
print(f"❌ {result['error']}")
|
830
|
+
else:
|
831
|
+
print(f"MIME: {result['mime_type']}")
|
832
|
+
print(f"Size: {result['size']} bytes")
|
833
|
+
print(f"Base64 data: {result['base64_data'][:100]}...")
|
834
|
+
|
835
|
+
elif args.command == 'set':
|
836
|
+
if not self.editor:
|
837
|
+
print("❌ No file loaded. Use 'load' command first.")
|
838
|
+
return
|
839
|
+
|
840
|
+
if args.type == 'text':
|
841
|
+
success = self.editor.set_element_text(args.xpath, args.value)
|
842
|
+
elif args.type == 'attribute':
|
843
|
+
if not args.attr:
|
844
|
+
print("❌ --attr required for attribute updates")
|
845
|
+
return
|
846
|
+
success = self.editor.set_element_attribute(args.xpath, args.attr, args.value)
|
847
|
+
|
848
|
+
if success:
|
849
|
+
print("✅ Element updated")
|
850
|
+
else:
|
851
|
+
print("❌ Element not found")
|
852
|
+
|
853
|
+
elif args.command == 'list':
|
854
|
+
if not self.editor:
|
855
|
+
print("❌ No file loaded. Use 'load' command first.")
|
856
|
+
return
|
857
|
+
|
858
|
+
elements = self.editor.list_elements(args.xpath)
|
859
|
+
if not elements:
|
860
|
+
print("No elements found")
|
861
|
+
return
|
862
|
+
|
863
|
+
print(f"Found {len(elements)} elements:")
|
864
|
+
for i, elem in enumerate(elements[:args.limit]):
|
865
|
+
print(f"\n{i + 1}. Path: {elem['path']}")
|
866
|
+
print(f" Tag: {elem['tag']}")
|
867
|
+
if elem['text']:
|
868
|
+
text_preview = elem['text'][:100] + "..." if len(elem['text']) > 100 else elem['text']
|
869
|
+
print(f" Text: {repr(text_preview)}")
|
870
|
+
if elem['attributes']:
|
871
|
+
print(f" Attributes: {elem['attributes']}")
|
872
|
+
|
873
|
+
if len(elements) > args.limit:
|
874
|
+
print(f"\n... and {len(elements) - args.limit} more (use --limit to see more)")
|
875
|
+
|
876
|
+
elif args.command == 'save':
|
877
|
+
if not self.editor:
|
878
|
+
print("❌ No file loaded. Use 'load' command first.")
|
879
|
+
return
|
880
|
+
|
881
|
+
if self.editor.is_remote and not args.output:
|
882
|
+
print("❌ Remote file requires --output parameter")
|
883
|
+
return
|
884
|
+
|
885
|
+
success = self.editor.save(args.output)
|
886
|
+
if success:
|
887
|
+
save_path = args.output or self.editor.file_path
|
888
|
+
print(f"✅ File saved to {save_path}")
|
889
|
+
else:
|
890
|
+
print("❌ Save failed")
|
891
|
+
|
892
|
+
except Exception as e:
|
893
|
+
print(f"❌ Error: {e}")
|
894
|
+
|
895
|
+
def start_shell(self):
|
896
|
+
"""Uruchom interaktywny shell"""
|
897
|
+
print("🚀 Interactive File Editor Shell")
|
898
|
+
print("Commands:")
|
899
|
+
print(" load <file> - Load file (local path or URL)")
|
900
|
+
print(" query <xpath> - Query element text")
|
901
|
+
print(" attr <xpath> <name> - Get element attribute")
|
902
|
+
print(" set <xpath> <value> - Set element text")
|
903
|
+
print(" setattr <xpath> <name> <value> - Set element attribute")
|
904
|
+
print(" extract <xpath> - Show Data URI info")
|
905
|
+
print(" save [path] - Save file")
|
906
|
+
print(" list [xpath] - List elements")
|
907
|
+
print(" help - Show this help")
|
908
|
+
print(" exit - Exit shell")
|
909
|
+
print()
|
910
|
+
|
911
|
+
while True:
|
912
|
+
try:
|
913
|
+
command_line = input("📝 > ").strip()
|
914
|
+
if not command_line:
|
915
|
+
continue
|
916
|
+
|
917
|
+
if command_line == 'exit':
|
918
|
+
break
|
919
|
+
elif command_line == 'help':
|
920
|
+
print("Available commands: load, query, attr, set, setattr, extract, save, list, help, exit")
|
921
|
+
continue
|
922
|
+
|
923
|
+
parts = command_line.split()
|
924
|
+
command = parts[0]
|
925
|
+
|
926
|
+
if command == 'load' and len(parts) == 2:
|
927
|
+
self.editor = FileEditor(parts[1])
|
928
|
+
file_type = "remote" if parts[1].startswith('http') else "local"
|
929
|
+
print(f"✅ Loaded {file_type} file ({self.editor.file_type})")
|
930
|
+
|
931
|
+
elif command == 'query' and len(parts) >= 2:
|
932
|
+
if not self.editor:
|
933
|
+
print("❌ No file loaded")
|
934
|
+
continue
|
935
|
+
xpath = ' '.join(parts[1:])
|
936
|
+
result = self.editor.get_element_text(xpath)
|
937
|
+
print(f"Result: {result}")
|
938
|
+
|
939
|
+
elif command == 'attr' and len(parts) >= 3:
|
940
|
+
if not self.editor:
|
941
|
+
print("❌ No file loaded")
|
942
|
+
continue
|
943
|
+
xpath = parts[1]
|
944
|
+
attr_name = parts[2]
|
945
|
+
result = self.editor.get_element_attribute(xpath, attr_name)
|
946
|
+
print(f"Attribute {attr_name}: {result}")
|
947
|
+
|
948
|
+
elif command == 'set' and len(parts) >= 3:
|
949
|
+
if not self.editor:
|
950
|
+
print("❌ No file loaded")
|
951
|
+
continue
|
952
|
+
xpath = parts[1]
|
953
|
+
value = ' '.join(parts[2:])
|
954
|
+
success = self.editor.set_element_text(xpath, value)
|
955
|
+
print("✅ Updated" if success else "❌ Not found")
|
956
|
+
|
957
|
+
elif command == 'setattr' and len(parts) >= 4:
|
958
|
+
if not self.editor:
|
959
|
+
print("❌ No file loaded")
|
960
|
+
continue
|
961
|
+
xpath = parts[1]
|
962
|
+
attr_name = parts[2]
|
963
|
+
attr_value = ' '.join(parts[3:])
|
964
|
+
success = self.editor.set_element_attribute(xpath, attr_name, attr_value)
|
965
|
+
print("✅ Updated" if success else "❌ Not found")
|
966
|
+
|
967
|
+
elif command == 'extract' and len(parts) >= 2:
|
968
|
+
if not self.editor:
|
969
|
+
print("❌ No file loaded")
|
970
|
+
continue
|
971
|
+
xpath = ' '.join(parts[1:])
|
972
|
+
result = self.editor.extract_data_uri(xpath)
|
973
|
+
if "error" in result:
|
974
|
+
print(f"❌ {result['error']}")
|
975
|
+
else:
|
976
|
+
print(f"✅ MIME: {result['mime_type']}, Size: {result['size']} bytes")
|
977
|
+
|
978
|
+
elif command == 'list':
|
979
|
+
if not self.editor:
|
980
|
+
print("❌ No file loaded")
|
981
|
+
continue
|
982
|
+
xpath = parts[1] if len(parts) > 1 else "//*"
|
983
|
+
elements = self.editor.list_elements(xpath)[:10] # limit to 10
|
984
|
+
for elem in elements:
|
985
|
+
text_preview = elem['text'][:50] + "..." if len(elem['text']) > 50 else elem['text']
|
986
|
+
print(f"{elem['tag']}: {repr(text_preview)}")
|
987
|
+
|
988
|
+
elif command == 'save':
|
989
|
+
if not self.editor:
|
990
|
+
print("❌ No file loaded")
|
991
|
+
continue
|
992
|
+
output = parts[1] if len(parts) > 1 else None
|
993
|
+
if self.editor.is_remote and not output:
|
994
|
+
print("❌ Remote file requires output path")
|
995
|
+
continue
|
996
|
+
success = self.editor.save(output)
|
997
|
+
print("✅ Saved" if success else "❌ Save failed")
|
998
|
+
|
999
|
+
else:
|
1000
|
+
print("❌ Unknown command or wrong arguments. Type 'help' for available commands.")
|
1001
|
+
|
1002
|
+
except KeyboardInterrupt:
|
1003
|
+
print("\n👋 Goodbye!")
|
1004
|
+
break
|
1005
|
+
except Exception as e:
|
1006
|
+
print(f"❌ Error: {e}")
|
1007
|
+
|
1008
|
+
def start_server(self, host, port):
|
1009
|
+
"""Uruchom serwer HTTP"""
|
1010
|
+
print(f"🌐 Starting File Editor Server on {host}:{port}")
|
1011
|
+
print(f"Open http://{host}:{port} in your browser")
|
1012
|
+
print("API endpoints:")
|
1013
|
+
print(f" GET http://{host}:{port}/api/extract?url=<URL>&xpath=<XPATH>")
|
1014
|
+
print(f" POST http://{host}:{port}/api/load")
|
1015
|
+
print(f" POST http://{host}:{port}/api/query")
|
1016
|
+
print(f" POST http://{host}:{port}/api/update")
|
1017
|
+
print(f" POST http://{host}:{port}/api/save")
|
1018
|
+
print()
|
1019
|
+
|
1020
|
+
server = HTTPServer((host, port), FileEditorServer)
|
1021
|
+
try:
|
1022
|
+
server.serve_forever()
|
1023
|
+
except KeyboardInterrupt:
|
1024
|
+
print("\n👋 Server stopped")
|
1025
|
+
|
1026
|
+
def create_examples(self, directory):
|
1027
|
+
"""Utwórz przykładowe pliki do testowania"""
|
1028
|
+
base_path = Path(directory)
|
1029
|
+
base_path.mkdir(exist_ok=True)
|
1030
|
+
|
1031
|
+
# Przykładowy SVG z Data URI
|
1032
|
+
svg_with_data_uri = '''<?xml version="1.0" encoding="UTF-8"?>
|
1033
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="300">
|
1034
|
+
<metadata>
|
1035
|
+
{
|
1036
|
+
"title": "Example SVG with Data URI",
|
1037
|
+
"description": "Test file for editor",
|
1038
|
+
"author": "File Editor Tool"
|
1039
|
+
}
|
1040
|
+
</metadata>
|
1041
|
+
<rect x="10" y="10" width="50" height="50" fill="red" id="square1"/>
|
1042
|
+
<circle cx="100" cy="100" r="30" fill="blue" id="circle1"/>
|
1043
|
+
<text x="50" y="150" id="text1" font-size="16">Hello World</text>
|
1044
|
+
|
1045
|
+
<!-- Embedded PDF as Data URI -->
|
1046
|
+
<image x="200" y="50" width="150" height="100"
|
1047
|
+
xlink:href="data:application/pdf;base64,JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovUGFnZXMgMiAwIFIKPj4KZW5kb2JqCjIgMCBvYmoKPDwKL1R5cGUgL1BhZ2VzCi9LaWRzIFszIDAgUl0KL0NvdW50IDEKPD4KZW5kb2JqCjMgMCBvYmoKPDwKL1R5cGUgL1BhZ2UKL1BhcmVudCAyIDAgUgovTWVkaWFCb3ggWzAgMCA2MTIgNzkyXQovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA0IDAgUgo+Pgo+PgovQ29udGVudHMgNSAwIFIKPj4KZW5kb2JqCjQgMCBvYmoKPDwKL1R5cGUgL0ZvbnQKL1N1YnR5cGUgL1R5cGUxCi9CYXNlRm9udCAvSGVsdmV0aWNhCj4+CmVuZG9iago1IDAgb2JqCjw8Ci9MZW5ndGggNDQKPj4Kc3RyZWFtCkJUCi9GMSAxMiBUZgoxMDAgNzAwIFRkCihIZWxsbyBXb3JsZCEpIFRqCkVUCmVuZHN0cmVhbQplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQ1IDAwMDAwIG4gCjAwMDAwMDAzMTYgMDAwMDAgbiAKdHJhaWxlcgo8PAovU2l6ZSA2Ci9Sb290IDEgMCBSCj4+CnN0YXJ0eHJlZgo0MTAKJSVFT0Y=" />
|
1048
|
+
|
1049
|
+
<!-- JSON data in foreignObject -->
|
1050
|
+
<foreignObject x="0" y="250" width="400" height="50">
|
1051
|
+
<script type="application/json">
|
1052
|
+
{
|
1053
|
+
"data": [10, 20, 30, 40],
|
1054
|
+
"labels": ["A", "B", "C", "D"],
|
1055
|
+
"config": {"type": "chart", "animated": true}
|
1056
|
+
}
|
1057
|
+
</script>
|
1058
|
+
</foreignObject>
|
1059
|
+
</svg>'''
|
1060
|
+
|
1061
|
+
# Przykładowy XML z danymi
|
1062
|
+
xml_with_data = '''<?xml version="1.0" encoding="UTF-8"?>
|
1063
|
+
<data xmlns:xlink="http://www.w3.org/1999/xlink">
|
1064
|
+
<metadata>
|
1065
|
+
<title>Example Data XML</title>
|
1066
|
+
<version>1.0</version>
|
1067
|
+
<created>2025-01-01</created>
|
1068
|
+
</metadata>
|
1069
|
+
<records>
|
1070
|
+
<record id="1" type="user">
|
1071
|
+
<name>John Doe</name>
|
1072
|
+
<age>30</age>
|
1073
|
+
<email>john@example.com</email>
|
1074
|
+
<avatar xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" />
|
1075
|
+
</record>
|
1076
|
+
<record id="2" type="admin">
|
1077
|
+
<name>Jane Smith</name>
|
1078
|
+
<age>25</age>
|
1079
|
+
<email>jane@example.com</email>
|
1080
|
+
<settings>{"theme": "dark", "notifications": true}</settings>
|
1081
|
+
</record>
|
1082
|
+
</records>
|
1083
|
+
<files>
|
1084
|
+
<file name="document.pdf"
|
1085
|
+
data="data:application/pdf;base64,JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovUGFnZXMgMiAwIFIKPj4KZW5kb2JqCjIgMCBvYmoKPDwKL1R5cGUgL1BhZ2VzCi9LaWRzIFszIDAgUl0KL0NvdW50IDEKPD4KZW5kb2JqCjMgMCBvYmoKPDwKL1R5cGUgL1BhZ2UKL1BhcmVudCAyIDAgUgovTWVkaWFCb3ggWzAgMCA2MTIgNzkyXQovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA0IDAgUgo+Pgo+PgovQ29udGVudHMgNSAwIFIKPj4KZW5kb2JqCjQgMCBvYmoKPDwKL1R5cGUgL0ZvbnQKL1N1YnR5cGUgL1R5cGUxCi9CYXNlRm9udCAvSGVsdmV0aWNhCj4+CmVuZG9iago1IDAgb2JqCjw8Ci9MZW5ndGggNDQKPj4Kc3RyZWFtCkJUCi9GMSAxMiBUZgoxMDAgNzAwIFRkCihIZWxsbyBXb3JsZCEpIFRqCkVUCmVuZHN0cmVhbQplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQ1IDAwMDAwIG4gCjAwMDAwMDAzMTYgMDAwMDAgbiAKdHJhaWxlcgo8PAovU2l6ZSA2Ci9Sb290IDEgMCBSCj4+CnN0YXJ0eHJlZgo0MTAKJSVFT0Y=" />
|
1086
|
+
</files>
|
1087
|
+
</data>'''
|
1088
|
+
|
1089
|
+
# Przykładowy HTML z osadzonym SVG
|
1090
|
+
html_with_svg = '''<!DOCTYPE html>
|
1091
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
1092
|
+
<head>
|
1093
|
+
<title>Example HTML with embedded SVG</title>
|
1094
|
+
<meta name="description" content="Test HTML file with SVG and data">
|
1095
|
+
<style>
|
1096
|
+
body { font-family: Arial, sans-serif; margin: 20px; }
|
1097
|
+
.data-container { background: #f0f0f0; padding: 10px; margin: 10px 0; }
|
1098
|
+
</style>
|
1099
|
+
</head>
|
1100
|
+
<body>
|
1101
|
+
<h1 id="main-title">Example HTML Document</h1>
|
1102
|
+
|
1103
|
+
<div class="content">
|
1104
|
+
<p id="intro">This HTML contains embedded SVG with Data URI.</p>
|
1105
|
+
|
1106
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
1107
|
+
width="300" height="200" id="embedded-svg">
|
1108
|
+
<rect x="10" y="10" width="100" height="50" fill="green" />
|
1109
|
+
<text x="20" y="35" fill="white">Embedded SVG</text>
|
1110
|
+
<image x="150" y="20" width="100" height="60"
|
1111
|
+
xlink:href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjYwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iNjAiIGZpbGw9InJlZCIvPjx0ZXh0IHg9IjEwIiB5PSIzNSIgZmlsbD0id2hpdGUiPkRhdGEgVVJJPC90ZXh0Pjwvc3ZnPg==" />
|
1112
|
+
</svg>
|
1113
|
+
|
1114
|
+
<div class="data-container">
|
1115
|
+
<h3>JSON Data</h3>
|
1116
|
+
<script type="application/json" id="page-data">
|
1117
|
+
{
|
1118
|
+
"page": "example",
|
1119
|
+
"data": {
|
1120
|
+
"users": [
|
1121
|
+
{"id": 1, "name": "Alice"},
|
1122
|
+
{"id": 2, "name": "Bob"}
|
1123
|
+
],
|
1124
|
+
"settings": {
|
1125
|
+
"theme": "light",
|
1126
|
+
"language": "en"
|
1127
|
+
}
|
1128
|
+
}
|
1129
|
+
}
|
1130
|
+
</script>
|
1131
|
+
</div>
|
1132
|
+
|
1133
|
+
<ul>
|
1134
|
+
<li class="item" data-value="1">Item 1</li>
|
1135
|
+
<li class="item" data-value="2">Item 2</li>
|
1136
|
+
<li class="item" data-value="3">Item 3</li>
|
1137
|
+
</ul>
|
1138
|
+
</div>
|
1139
|
+
</body>
|
1140
|
+
</html>'''
|
1141
|
+
|
1142
|
+
# Zapisz pliki
|
1143
|
+
files = [
|
1144
|
+
('example.svg', svg_with_data_uri),
|
1145
|
+
('example.xml', xml_with_data),
|
1146
|
+
('example.html', html_with_svg)
|
1147
|
+
]
|
1148
|
+
|
1149
|
+
for filename, content in files:
|
1150
|
+
file_path = base_path / filename
|
1151
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
1152
|
+
f.write(content)
|
1153
|
+
print(f"✅ Created: {file_path}")
|
1154
|
+
|
1155
|
+
# Utwórz plik z przykładami użycia
|
1156
|
+
examples_usage = '''# File Editor Tool - Examples
|
1157
|
+
|
1158
|
+
## Example XPath queries for created files:
|
1159
|
+
|
1160
|
+
### example.svg:
|
1161
|
+
- Get text content: `//svg:text[@id='text1']`
|
1162
|
+
- Get image Data URI: `//svg:image/@xlink:href`
|
1163
|
+
- Get JSON metadata: `//svg:metadata`
|
1164
|
+
- Get all elements with fill attribute: `//*[@fill]`
|
1165
|
+
|
1166
|
+
### example.xml:
|
1167
|
+
- Get user names: `//record[@type='user']/name`
|
1168
|
+
- Get file Data URI: `//file[@name='document.pdf']/@data`
|
1169
|
+
- Get user by ID: `//record[@id='1']`
|
1170
|
+
- Get JSON settings: `//record[@id='2']/settings`
|
1171
|
+
|
1172
|
+
### example.html:
|
1173
|
+
- Get page title: `//html:h1[@id='main-title']`
|
1174
|
+
- Get embedded SVG Data URI: `//svg:image/@xlink:href`
|
1175
|
+
- Get JSON data: `//script[@type='application/json']`
|
1176
|
+
- Get list items: `//html:li[@class='item']`
|
1177
|
+
|
1178
|
+
## CLI Usage Examples:
|
1179
|
+
|
1180
|
+
```bash
|
1181
|
+
# Load and explore SVG file
|
1182
|
+
python file_editor.py load example.svg
|
1183
|
+
python file_editor.py list --xpath "//svg:*"
|
1184
|
+
python file_editor.py query "//svg:text[@id='text1']"
|
1185
|
+
|
1186
|
+
# Extract Data URI from SVG
|
1187
|
+
python file_editor.py extract "//svg:image/@xlink:href" --info
|
1188
|
+
python file_editor.py extract "//svg:image/@xlink:href" --output extracted.pdf
|
1189
|
+
|
1190
|
+
# Edit elements
|
1191
|
+
python file_editor.py set "//svg:text[@id='text1']" "Modified Text"
|
1192
|
+
python file_editor.py save --output modified.svg
|
1193
|
+
|
1194
|
+
# Work with remote files
|
1195
|
+
python file_editor.py load "http://example.com/file.svg"
|
1196
|
+
python file_editor.py extract "//svg:image/@xlink:href" --info
|
1197
|
+
|
1198
|
+
# Start server
|
1199
|
+
python file_editor.py server --port 8080
|
1200
|
+
# Then open http://localhost:8080 in browser
|
1201
|
+
```
|
1202
|
+
|
1203
|
+
## Server API Examples:
|
1204
|
+
|
1205
|
+
```bash
|
1206
|
+
# Extract Data URI from remote file
|
1207
|
+
curl "http://localhost:8080/api/extract?url=http://example.com/file.svg&xpath=//svg:image/@xlink:href"
|
1208
|
+
|
1209
|
+
# Load file via POST
|
1210
|
+
curl -X POST http://localhost:8080/api/load \\
|
1211
|
+
-H "Content-Type: application/json" \\
|
1212
|
+
-d '{"file_path": "example.svg"}'
|
1213
|
+
|
1214
|
+
# Query elements
|
1215
|
+
curl -X POST http://localhost:8080/api/query \\
|
1216
|
+
-H "Content-Type: application/json" \\
|
1217
|
+
-d '{"query": "//svg:text", "type": "xpath"}'
|
1218
|
+
```
|
1219
|
+
'''
|
1220
|
+
|
1221
|
+
usage_path = base_path / 'EXAMPLES.md'
|
1222
|
+
with open(usage_path, 'w', encoding='utf-8') as f:
|
1223
|
+
f.write(examples_usage)
|
1224
|
+
print(f"✅ Created: {usage_path}")
|
1225
|
+
|
1226
|
+
print(f"\n🎉 Example files created in {base_path}")
|
1227
|
+
print("Use 'python file_editor.py examples --help' to see usage examples")
|
1228
|
+
|
1229
|
+
|
1230
|
+
def main():
|
1231
|
+
"""Główna funkcja"""
|
1232
|
+
cli = CLI()
|
1233
|
+
cli.run()
|
1234
|
+
|
1235
|
+
|
1236
|
+
if __name__ == "__main__":
|
1237
|
+
main()
|