Update methods
Browse files- app.py +59 -280
- static/script.js +0 -196
- static/style.css +0 -171
- templates/index.html +0 -42
app.py
CHANGED
@@ -1,26 +1,20 @@
|
|
1 |
import requests
|
2 |
-
import json
|
3 |
import os
|
4 |
-
import uuid
|
5 |
import zipfile
|
6 |
-
import
|
7 |
import subprocess
|
8 |
import os
|
9 |
import re
|
10 |
import warnings
|
11 |
from fastapi import FastAPI, HTTPException
|
12 |
from fastapi.middleware.cors import CORSMiddleware
|
13 |
-
from fastapi.responses import FileResponse
|
14 |
-
from fastapi.staticfiles import StaticFiles
|
15 |
from pydantic import BaseModel
|
16 |
-
from typing import Any, Dict, List, Literal, Optional
|
17 |
|
18 |
warnings.filterwarnings("ignore")
|
19 |
|
20 |
app = FastAPI(title="3GPP Specification Splitter API",
|
21 |
-
description="API to split and display specifications by their chapters & sub-chapters"
|
22 |
-
|
23 |
-
app.mount("/static", StaticFiles(directory="static"), name="static")
|
24 |
|
25 |
origins = [
|
26 |
"*",
|
@@ -34,73 +28,49 @@ app.add_middleware(
|
|
34 |
allow_headers=["*"],
|
35 |
)
|
36 |
|
37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
|
39 |
-
|
40 |
-
|
41 |
-
doc_id = specification
|
42 |
-
series = doc_id.split(".")[0]
|
43 |
|
|
|
44 |
response = requests.get(
|
45 |
-
|
46 |
verify=False,
|
47 |
-
headers={"User-Agent":
|
|
|
48 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
zip_bytes = io.BytesIO(zf.read(file_name))
|
60 |
-
zf = zipfile.ZipFile(zip_bytes)
|
61 |
-
for file_name2 in zf.namelist():
|
62 |
-
if file_name2.endswith("doc") or file_name2.endswith("docx"):
|
63 |
-
if "cover" in file_name2.lower():
|
64 |
-
print("COVER !")
|
65 |
-
continue
|
66 |
-
ext = file_name2.split(".")[-1]
|
67 |
-
doc_bytes = zf.read(file_name2)
|
68 |
-
temp_id = str(uuid.uuid4())
|
69 |
-
input_path = f"/tmp/{temp_id}.{ext}"
|
70 |
-
output_path = f"/tmp/{temp_id}.txt"
|
71 |
-
|
72 |
-
with open(input_path, "wb") as f:
|
73 |
-
f.write(doc_bytes)
|
74 |
|
75 |
-
subprocess.run([
|
76 |
-
"libreoffice",
|
77 |
-
"--headless",
|
78 |
-
"--convert-to", "txt",
|
79 |
-
"--outdir", "/tmp",
|
80 |
-
input_path
|
81 |
-
], check=True)
|
82 |
-
|
83 |
-
with open(output_path, "r") as f:
|
84 |
-
txt_data = [line.strip() for line in f if line.strip()]
|
85 |
-
|
86 |
-
os.remove(input_path)
|
87 |
-
os.remove(output_path)
|
88 |
-
return txt_data
|
89 |
-
elif file_name.endswith("doc") or file_name.endswith("docx"):
|
90 |
-
if "cover" in file_name.lower():
|
91 |
-
print("COVER !")
|
92 |
-
continue
|
93 |
-
ext = file_name.split(".")[-1]
|
94 |
-
doc_bytes = zf.read(file_name)
|
95 |
-
temp_id = str(uuid.uuid4())
|
96 |
-
input_path = f"/tmp/{temp_id}.{ext}"
|
97 |
-
output_path = f"/tmp/{temp_id}.txt"
|
98 |
-
|
99 |
-
print("Ecriture")
|
100 |
-
with open(input_path, "wb") as f:
|
101 |
-
f.write(doc_bytes)
|
102 |
-
|
103 |
-
print("Convertissement")
|
104 |
subprocess.run([
|
105 |
"libreoffice",
|
106 |
"--headless",
|
@@ -108,224 +78,33 @@ def get_text(specification: str, version: str):
|
|
108 |
"--outdir", "/tmp",
|
109 |
input_path
|
110 |
], check=True)
|
111 |
-
|
112 |
-
|
113 |
-
with open(output_path, "r", encoding="utf-8") as f:
|
114 |
txt_data = [line.strip() for line in f if line.strip()]
|
115 |
-
|
116 |
os.remove(input_path)
|
117 |
os.remove(output_path)
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
def get_latest_version(spec: str) -> str:
|
123 |
-
try:
|
124 |
-
req = requests.post("https://organizedprogrammers-3gppdocfinder.hf.space/find", headers={"Content-Type": "application/json"}, data=json.dumps({"doc_id": spec}), verify=False)
|
125 |
-
except Exception as e:
|
126 |
-
raise HTTPException(500, f"An error has occured while getting latest version: {e}")
|
127 |
-
if req.status_code == 200:
|
128 |
-
reqJS = req.json()
|
129 |
-
return reqJS['version']
|
130 |
else:
|
131 |
-
|
132 |
-
|
133 |
-
def create_nested_structure(chapters, text, real_toc_indexes):
|
134 |
-
"""Crée une structure hiérarchique où les sous-sections sont imbriquées dans leurs sections parentes."""
|
135 |
-
result = {}
|
136 |
-
|
137 |
-
# Trier les chapitres par numéro de section
|
138 |
-
sorted_chapters = sorted(chapters, key=lambda x: [int(p) if p.isdigit() else p for p in x.split()[0].split('.')])
|
139 |
-
|
140 |
-
# Préparer les contenus des chapitres
|
141 |
-
chapter_contents = {}
|
142 |
-
for i, chapter in enumerate(sorted_chapters):
|
143 |
-
current_index = real_toc_indexes[chapter]
|
144 |
-
|
145 |
-
# Déterminer l'index de fin
|
146 |
-
end_index = len(text)
|
147 |
-
if i < len(sorted_chapters) - 1:
|
148 |
-
next_chapter = sorted_chapters[i + 1]
|
149 |
-
end_index = real_toc_indexes[next_chapter]
|
150 |
-
|
151 |
-
# Extraire et nettoyer le contenu
|
152 |
-
content = text[current_index + 1:end_index]
|
153 |
-
cleaned_content = "\n".join(content).strip()
|
154 |
-
chapter_contents[chapter] = cleaned_content
|
155 |
-
|
156 |
-
# Fonction récursive pour construire la structure hiérarchique
|
157 |
-
def insert_section(root, section_path, title, content):
|
158 |
-
"""Insère une section dans l'arborescence hiérarchique."""
|
159 |
-
parts = section_path.split('.')
|
160 |
-
|
161 |
-
# Ignorer les sections sans titre réel (seulement si le titre est vide ou juste des espaces)
|
162 |
-
if not title.strip():
|
163 |
-
# Si c'est une section sans titre mais avec du contenu, on peut le fusionner avec sa première sous-section
|
164 |
-
# ou simplement l'ignorer selon votre besoin
|
165 |
-
return None
|
166 |
-
|
167 |
-
# Cas de base: section de premier niveau
|
168 |
-
if len(parts) == 1:
|
169 |
-
key = section_path + " " + title
|
170 |
-
if key not in root:
|
171 |
-
root[key] = {"content": content, "subsections": {}}
|
172 |
-
else:
|
173 |
-
root[key]["content"] = content
|
174 |
-
return root[key]
|
175 |
-
|
176 |
-
# Trouver ou créer le parent
|
177 |
-
parent_path = '.'.join(parts[:-1])
|
178 |
-
for key in root.keys():
|
179 |
-
if key.startswith(parent_path + " "):
|
180 |
-
# Parent trouvé, insérer dans ses sous-sections
|
181 |
-
section_key = section_path + " " + title
|
182 |
-
if section_key not in root[key]["subsections"]:
|
183 |
-
root[key]["subsections"][section_key] = {"content": content, "subsections": {}}
|
184 |
-
else:
|
185 |
-
root[key]["subsections"][section_key]["content"] = content
|
186 |
-
return root[key]["subsections"][section_key]
|
187 |
-
|
188 |
-
# Parent non trouvé, il faut le créer d'abord
|
189 |
-
# Rechercher le titre du parent
|
190 |
-
parent_title = ""
|
191 |
-
for chapter in sorted_chapters:
|
192 |
-
if chapter.split()[0] == parent_path:
|
193 |
-
parts = chapter.split(maxsplit=1)
|
194 |
-
parent_title = parts[1] if len(parts) > 1 else ""
|
195 |
-
break
|
196 |
-
|
197 |
-
# Si le parent n'a pas de titre, on cherche un parent plus haut
|
198 |
-
if not parent_title.strip():
|
199 |
-
# On peut soit ignorer cette branche, soit essayer de trouver un parent valide plus haut
|
200 |
-
grand_parent_parts = parent_path.split('.')
|
201 |
-
if len(grand_parent_parts) > 1:
|
202 |
-
grand_parent_path = '.'.join(grand_parent_parts[:-1])
|
203 |
-
for key in root.keys():
|
204 |
-
if key.startswith(grand_parent_path + " "):
|
205 |
-
# On a trouvé un grand-parent valide, on insère directement dedans
|
206 |
-
section_key = section_path + " " + title
|
207 |
-
if section_key not in root[key]["subsections"]:
|
208 |
-
root[key]["subsections"][section_key] = {"content": content, "subsections": {}}
|
209 |
-
return root[key]["subsections"][section_key]
|
210 |
-
# Si on n'a pas trouvé de grand-parent valide, on insère à la racine
|
211 |
-
section_key = section_path + " " + title
|
212 |
-
root[section_key] = {"content": content, "subsections": {}}
|
213 |
-
return root[section_key]
|
214 |
-
|
215 |
-
# Créer le parent récursivement
|
216 |
-
parent_section = insert_section(root, parent_path, parent_title, "")
|
217 |
-
|
218 |
-
# Si le parent n'a pas pu être créé (car sans titre), on insère à la racine
|
219 |
-
if parent_section is None:
|
220 |
-
section_key = section_path + " " + title
|
221 |
-
root[section_key] = {"content": content, "subsections": {}}
|
222 |
-
return root[section_key]
|
223 |
-
|
224 |
-
# Maintenant insérer cette section dans le parent nouvellement créé
|
225 |
-
section_key = section_path + " " + title
|
226 |
-
parent_section["subsections"][section_key] = {"content": content, "subsections": {}}
|
227 |
-
return parent_section["subsections"][section_key]
|
228 |
-
|
229 |
-
# Traiter chaque chapitre
|
230 |
-
for chapter in sorted_chapters:
|
231 |
-
parts = chapter.split(maxsplit=1)
|
232 |
-
section_num = parts[0]
|
233 |
-
section_title = parts[1] if len(parts) > 1 else ""
|
234 |
-
|
235 |
-
# Ne traiter que les sections avec un titre
|
236 |
-
if section_title.strip():
|
237 |
-
insert_section(result, section_num, section_title, chapter_contents[chapter])
|
238 |
-
|
239 |
-
return result
|
240 |
-
|
241 |
-
class SpecRequest(BaseModel):
|
242 |
-
specification: str
|
243 |
-
version: Optional[str] = None
|
244 |
|
245 |
-
@app.
|
246 |
-
def
|
247 |
-
|
248 |
-
|
249 |
-
@app.post("/online/plain")
|
250 |
-
def get_file_from_spec_id_version(req: SpecRequest) -> Dict[str, str]:
|
251 |
-
spec = req.specification
|
252 |
-
version = req.version
|
253 |
-
if not version:
|
254 |
-
version = get_latest_version(spec)
|
255 |
-
|
256 |
-
text = get_text(spec, version)
|
257 |
-
forewords = []
|
258 |
-
for x in range(len(text)):
|
259 |
-
line = text[x]
|
260 |
-
if "Foreword" in line:
|
261 |
-
forewords.append(x)
|
262 |
-
if len(forewords) >= 2:
|
263 |
-
break
|
264 |
-
|
265 |
-
toc_brut = text[forewords[1]:]
|
266 |
chapters = []
|
267 |
-
|
268 |
-
x = line.split("\t")
|
269 |
-
m = re.search(regex, line)
|
270 |
-
if m and any(line in c for c in text[forewords[0]:forewords[1]]):
|
271 |
-
chapters.append(line)
|
272 |
-
print(line)
|
273 |
-
|
274 |
-
real_toc_indexes = {}
|
275 |
|
276 |
-
for
|
277 |
-
|
278 |
-
|
279 |
|
280 |
document = {}
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
document[
|
286 |
-
curr_index = x
|
287 |
-
|
288 |
-
document[toc[curr_index].replace("\t", " ")] = re.sub(r"\s+", " ", " ".join(text[index_toc[curr_index]+1:]))
|
289 |
-
return document
|
290 |
-
|
291 |
-
@app.post("/online")
|
292 |
-
def get_file_from_spec_id_version(req: SpecRequest) -> Dict:
|
293 |
-
spec = req.specification
|
294 |
-
version = req.version
|
295 |
-
if not version:
|
296 |
-
version = get_latest_version(spec)
|
297 |
|
298 |
-
|
299 |
-
forewords = []
|
300 |
-
for x in range(len(text)):
|
301 |
-
line = text[x]
|
302 |
-
if "Foreword" in line:
|
303 |
-
forewords.append(x)
|
304 |
-
if len(forewords) >= 2:
|
305 |
-
break
|
306 |
-
|
307 |
-
toc_brut = text[forewords[1]:]
|
308 |
-
chapters = []
|
309 |
-
for line in toc_brut:
|
310 |
-
x = line.split("\t")
|
311 |
-
m = re.search(regex, line)
|
312 |
-
if m and any(line in c for c in text[forewords[0]:forewords[1]]):
|
313 |
-
chapters.append(line)
|
314 |
-
print(line)
|
315 |
-
|
316 |
-
real_toc_indexes = {}
|
317 |
-
|
318 |
-
for chapter in chapters:
|
319 |
-
x = text.index(chapter)
|
320 |
-
real_toc_indexes[chapter] = x
|
321 |
-
|
322 |
-
document = {}
|
323 |
-
toc = list(real_toc_indexes.keys())
|
324 |
-
index_toc = list(real_toc_indexes.values())
|
325 |
-
curr_index = 0
|
326 |
-
for x in range(1, len(toc)):
|
327 |
-
document[toc[curr_index].replace("\t", " ")] = re.sub(r"[\ \t]+", " ", "\n".join(text[index_toc[curr_index]+1:index_toc[x]]))
|
328 |
-
curr_index = x
|
329 |
-
|
330 |
-
document[toc[curr_index].replace("\t", " ")] = re.sub(r"\s+", " ", " ".join(text[index_toc[curr_index]+1:]))
|
331 |
-
return create_nested_structure(chapters, text, real_toc_indexes)
|
|
|
1 |
import requests
|
|
|
2 |
import os
|
|
|
3 |
import zipfile
|
4 |
+
from io import BytesIO
|
5 |
import subprocess
|
6 |
import os
|
7 |
import re
|
8 |
import warnings
|
9 |
from fastapi import FastAPI, HTTPException
|
10 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
11 |
from pydantic import BaseModel
|
|
|
12 |
|
13 |
warnings.filterwarnings("ignore")
|
14 |
|
15 |
app = FastAPI(title="3GPP Specification Splitter API",
|
16 |
+
description="API to split and display specifications by their chapters & sub-chapters",
|
17 |
+
docs_url="/")
|
|
|
18 |
|
19 |
origins = [
|
20 |
"*",
|
|
|
28 |
allow_headers=["*"],
|
29 |
)
|
30 |
|
31 |
+
class SpecRequest(BaseModel):
|
32 |
+
spec_id: str
|
33 |
+
|
34 |
+
@app.post("/get_full_text")
|
35 |
+
def get_text(request: SpecRequest):
|
36 |
+
specification = request.spec_id
|
37 |
+
total_file = []
|
38 |
+
url = requests.post(
|
39 |
+
"https://organizedprogrammers-3gppdocfinder.hf.space/find",
|
40 |
+
verify=False,
|
41 |
+
headers={"Content-Type": "application/json"},
|
42 |
+
json={"doc_id": specification}
|
43 |
+
)
|
44 |
|
45 |
+
if url.status_code != 200:
|
46 |
+
raise HTTPException(404, detail="Not found")
|
|
|
|
|
47 |
|
48 |
+
url = url.json()['url']
|
49 |
response = requests.get(
|
50 |
+
url,
|
51 |
verify=False,
|
52 |
+
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"}
|
53 |
+
|
54 |
)
|
55 |
+
|
56 |
+
zip_bytes = BytesIO(response.content)
|
57 |
+
current_zip_file = zipfile.ZipFile(zip_bytes)
|
58 |
+
for file_info in current_zip_file.infolist():
|
59 |
+
if file_info.filename.endswith(".zip") and len(current_zip_file.namelist()) == 1:
|
60 |
+
nested_zip_bytes = BytesIO(current_zip_file.read(file_info.filename))
|
61 |
+
current_zip_file = zipfile.ZipFile(nested_zip_bytes)
|
62 |
+
break
|
63 |
|
64 |
+
for file_info in current_zip_file.infolist():
|
65 |
+
filename = file_info.filename
|
66 |
+
if (filename.endswith('.doc') or filename.endswith('.docx')) and ("cover" not in filename.lower() and "annex" not in filename.lower()):
|
67 |
+
doc_bytes = current_zip_file.read(filename)
|
68 |
+
ext = filename.split(".")[-1]
|
69 |
+
input_path = f"/tmp/{specification}.{ext}"
|
70 |
+
output_path = f"/tmp/{specification}.txt"
|
71 |
+
with open(input_path, "wb") as f:
|
72 |
+
f.write(doc_bytes)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
subprocess.run([
|
75 |
"libreoffice",
|
76 |
"--headless",
|
|
|
78 |
"--outdir", "/tmp",
|
79 |
input_path
|
80 |
], check=True)
|
81 |
+
|
82 |
+
with open(output_path, "r") as f:
|
|
|
83 |
txt_data = [line.strip() for line in f if line.strip()]
|
84 |
+
|
85 |
os.remove(input_path)
|
86 |
os.remove(output_path)
|
87 |
+
total_file.extend(txt_data)
|
88 |
+
if total_file == []:
|
89 |
+
raise HTTPException(status_code=404, detail="Not found !")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
else:
|
91 |
+
return total_file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
|
93 |
+
@app.post("/get_spec_content")
|
94 |
+
def get_spec_content(request: SpecRequest):
|
95 |
+
text = get_text(request)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
chapters = []
|
97 |
+
chapter_regex = re.compile(r"^(\d+[a-z]?(?:\.\d+)*)\t[A-Z0-9][\ \S]+$")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
|
99 |
+
for i, line in enumerate(text):
|
100 |
+
if chapter_regex.fullmatch(line):
|
101 |
+
chapters.append((i, line))
|
102 |
|
103 |
document = {}
|
104 |
+
for i in range(len(chapters)):
|
105 |
+
start_index, chapter_title = chapters[i]
|
106 |
+
end_index = chapters[i+1][0] if i+1 < len(chapters) else len(text)
|
107 |
+
content_lines = text[start_index + 1 : end_index]
|
108 |
+
document[chapter_title.replace('\t', " ")] = "\n".join(content_lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
|
110 |
+
return document
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/script.js
DELETED
@@ -1,196 +0,0 @@
|
|
1 |
-
const jsonTab = document.getElementById("tab1-btn")
|
2 |
-
const specTab = document.getElementById("tab2-btn")
|
3 |
-
|
4 |
-
document.getElementById("tab1").style.display = "block";
|
5 |
-
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
|
6 |
-
document.getElementById("versCheck").addEventListener('change', event=>{
|
7 |
-
if(event.target.checked){document.getElementById("versionInput").removeAttribute("disabled")} else {document.getElementById("versionInput").value = ""; document.getElementById("versionInput").setAttribute("disabled", "")}
|
8 |
-
})
|
9 |
-
function openTab(evt, tabName) {
|
10 |
-
var i, tabcontent, tablinks;
|
11 |
-
tabcontent = document.getElementsByClassName("tabcontent");
|
12 |
-
for (i = 0; i < tabcontent.length; i++) {
|
13 |
-
tabcontent[i].style.display = "none";
|
14 |
-
}
|
15 |
-
tablinks = document.getElementsByClassName("tablinks");
|
16 |
-
for (i = 0; i < tablinks.length; i++) {
|
17 |
-
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
18 |
-
}
|
19 |
-
document.getElementById(tabName).style.display = "block";
|
20 |
-
evt.currentTarget.className += " active";
|
21 |
-
}
|
22 |
-
|
23 |
-
function handleSpecSearch(){
|
24 |
-
let versCheck = document.getElementById("versCheck").checked;
|
25 |
-
let body = {"specification": document.getElementById("specInput").value};
|
26 |
-
if(versCheck && document.getElementById("versionInput").value.length > 0){
|
27 |
-
body["version"] = document.getElementById("versionInput").value;
|
28 |
-
}
|
29 |
-
|
30 |
-
fetch("/online", {
|
31 |
-
method: "POST",
|
32 |
-
headers: {"Content-Type": "application/json"},
|
33 |
-
body: JSON.stringify(body)
|
34 |
-
})
|
35 |
-
.then(data => data.json())
|
36 |
-
.then(resp => renderDocument("tab2", resp))
|
37 |
-
.catch(error => console.error(error))
|
38 |
-
}
|
39 |
-
|
40 |
-
function handleFileSelect(event) {
|
41 |
-
const file = event.target.files[0];
|
42 |
-
if (!file) return;
|
43 |
-
|
44 |
-
const fileNameMatch = file.name.match(/(\d+\.\d+(?:-\d+)?)[_-]([a-z0-9]+)\.json/i);
|
45 |
-
let fileInfoText = '';
|
46 |
-
|
47 |
-
if (fileNameMatch) {
|
48 |
-
const specNumber = fileNameMatch[1];
|
49 |
-
const versionCode = fileNameMatch[2];
|
50 |
-
|
51 |
-
// Conversion des caractères en numéro de version
|
52 |
-
let versionString = "";
|
53 |
-
for (let i = 0; i < versionCode.length; i++) {
|
54 |
-
let char = versionCode[i].toLowerCase();
|
55 |
-
let versionPart;
|
56 |
-
|
57 |
-
if (/[0-9]/.test(char)) {
|
58 |
-
versionPart = parseInt(char, 10);
|
59 |
-
} else if (/[a-z]/.test(char)) {
|
60 |
-
versionPart = char.charCodeAt(0) - 'a'.charCodeAt(0) + 10;
|
61 |
-
} else {
|
62 |
-
versionPart = "?";
|
63 |
-
}
|
64 |
-
|
65 |
-
if (i > 0) versionString += ".";
|
66 |
-
versionString += versionPart;
|
67 |
-
}
|
68 |
-
|
69 |
-
fileInfoText = `Spécification ${specNumber}, Version ${versionString}`;
|
70 |
-
document.getElementById('fileInfo').textContent = fileInfoText;
|
71 |
-
} else {
|
72 |
-
document.getElementById('fileInfo').textContent = file.name;
|
73 |
-
}
|
74 |
-
|
75 |
-
const reader = new FileReader();
|
76 |
-
reader.onload = function (e) {
|
77 |
-
try {
|
78 |
-
const jsonContent = JSON.parse(e.target.result);
|
79 |
-
renderDocument("tab1", jsonContent);
|
80 |
-
} catch (error) {
|
81 |
-
document.querySelector('#tab1 #document-container').innerHTML =
|
82 |
-
`<div class="error">Erreur lors du traitement du fichier JSON: ${error.message}</div>`;
|
83 |
-
}
|
84 |
-
};
|
85 |
-
reader.readAsText(file);
|
86 |
-
}
|
87 |
-
|
88 |
-
function renderDocument(tab, data) {
|
89 |
-
const container = document.querySelector(`#${tab} #document-container`);
|
90 |
-
container.innerHTML = '';
|
91 |
-
|
92 |
-
function renderSection(sectionKey, sectionData, level) {
|
93 |
-
const sectionDiv = document.createElement('div');
|
94 |
-
sectionDiv.className = `section level-${level}`;
|
95 |
-
|
96 |
-
// Extraire le numéro et le titre de la clé
|
97 |
-
const parts = sectionKey.split(/\s(.+)/); // Divise à partir du premier espace
|
98 |
-
const sectionNumber = parts[0];
|
99 |
-
const sectionTitle = parts[1] || "";
|
100 |
-
|
101 |
-
// Créer l'en-tête de section
|
102 |
-
const header = document.createElement(`h${Math.min(level + 1, 6)}`);
|
103 |
-
header.textContent = sectionKey; // Utiliser la clé complète comme titre
|
104 |
-
sectionDiv.appendChild(header);
|
105 |
-
|
106 |
-
// Ajouter le contenu
|
107 |
-
if (sectionData.content) {
|
108 |
-
const content = document.createElement('div');
|
109 |
-
content.className = 'content';
|
110 |
-
content.textContent = sectionData.content;
|
111 |
-
sectionDiv.appendChild(content);
|
112 |
-
}
|
113 |
-
|
114 |
-
// Ajouter les sous-sections récursivement
|
115 |
-
if (sectionData.subsections && Object.keys(sectionData.subsections).length > 0) {
|
116 |
-
const subsectionsDiv = document.createElement('div');
|
117 |
-
subsectionsDiv.className = 'subsections';
|
118 |
-
|
119 |
-
for (const [subKey, subData] of Object.entries(sectionData.subsections)) {
|
120 |
-
const subSection = renderSection(subKey, subData, level + 1);
|
121 |
-
subsectionsDiv.appendChild(subSection);
|
122 |
-
}
|
123 |
-
|
124 |
-
sectionDiv.appendChild(subsectionsDiv);
|
125 |
-
}
|
126 |
-
|
127 |
-
return sectionDiv;
|
128 |
-
}
|
129 |
-
|
130 |
-
// Parcourir les sections de premier niveau
|
131 |
-
for (const [sectionKey, sectionData] of Object.entries(data)) {
|
132 |
-
const sectionElement = renderSection(sectionKey, sectionData, 1);
|
133 |
-
container.appendChild(sectionElement);
|
134 |
-
}
|
135 |
-
}
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
function formatText(text) {
|
140 |
-
if (!text) return '';
|
141 |
-
|
142 |
-
// Remplacer les sauts de ligne
|
143 |
-
let formattedText = text.replace(/\n/g, '<br>');
|
144 |
-
|
145 |
-
// Formatage des tableaux (détection basique de structures tabulaires)
|
146 |
-
if (text.includes('Byte') && (text.includes('b8') || text.includes('b7'))) {
|
147 |
-
// Tenter de détecter et convertir les représentations de tables de bits
|
148 |
-
formattedText = formatBitTables(formattedText);
|
149 |
-
}
|
150 |
-
|
151 |
-
// Mise en évidence des termes techniques
|
152 |
-
formattedText = formattedText.replace(/\b([A-Z]{2,}(?:-[A-Z]+)*)\b/g, '<span class="code-block">$1</span>');
|
153 |
-
|
154 |
-
return formattedText;
|
155 |
-
}
|
156 |
-
|
157 |
-
function formatBitTables(text) {
|
158 |
-
// Exemple simple pour détecter et formater les tableaux de bits
|
159 |
-
// Une implémentation plus robuste nécessiterait une analyse plus complexe
|
160 |
-
|
161 |
-
// Détection basique d'un en-tête de table de bits
|
162 |
-
const tableHeaders = text.match(/b8\s+b7\s+b6\s+b5\s+b4\s+b3\s+b2\s+b1/g);
|
163 |
-
|
164 |
-
if (tableHeaders) {
|
165 |
-
// Remplacer les occurrences par une table HTML
|
166 |
-
tableHeaders.forEach(header => {
|
167 |
-
const tableStart =
|
168 |
-
'<table class="byte-table"><tr><th>b8</th><th>b7</th><th>b6</th><th>b5</th><th>b4</th><th>b3</th><th>b2</th><th>b1</th></tr>';
|
169 |
-
const tableEnd = '</table>';
|
170 |
-
|
171 |
-
// Essayer de capturer les lignes suivantes qui pourraient être des données de table
|
172 |
-
const headerPos = text.indexOf(header);
|
173 |
-
const nextLineStart = text.indexOf('<br>', headerPos) + 4;
|
174 |
-
let tableContent = '';
|
175 |
-
|
176 |
-
// Ajouter des lignes jusqu'à ce qu'on atteigne une ligne vide ou un nouveau paragraphe
|
177 |
-
let currentPos = nextLineStart;
|
178 |
-
let endPos = text.indexOf('<br><br>', currentPos);
|
179 |
-
if (endPos === -1) endPos = text.length;
|
180 |
-
|
181 |
-
const potentialTableData = text.substring(currentPos, endPos);
|
182 |
-
const rows = potentialTableData.split('<br>');
|
183 |
-
|
184 |
-
rows.forEach(row => {
|
185 |
-
if (row.trim() && !row.includes('Byte') && !row.includes('b8')) {
|
186 |
-
tableContent += '<tr><td colspan="8">' + row + '</td></tr>';
|
187 |
-
}
|
188 |
-
});
|
189 |
-
|
190 |
-
const tableHTML = tableStart + tableContent + tableEnd;
|
191 |
-
text = text.replace(header + potentialTableData, tableHTML);
|
192 |
-
});
|
193 |
-
}
|
194 |
-
|
195 |
-
return text;
|
196 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/style.css
DELETED
@@ -1,171 +0,0 @@
|
|
1 |
-
body {
|
2 |
-
font-family: Arial, sans-serif;
|
3 |
-
line-height: 1.6;
|
4 |
-
max-width: 1100px;
|
5 |
-
margin: 0 auto;
|
6 |
-
padding: 20px;
|
7 |
-
color: #333;
|
8 |
-
}
|
9 |
-
|
10 |
-
div[class^='section level']{
|
11 |
-
padding: 10px;
|
12 |
-
border: 1px solid #bbb;
|
13 |
-
}
|
14 |
-
|
15 |
-
.header {
|
16 |
-
margin-bottom: 30px;
|
17 |
-
padding-bottom: 15px;
|
18 |
-
}
|
19 |
-
|
20 |
-
.file-input-container {
|
21 |
-
margin-bottom: 20px;
|
22 |
-
}
|
23 |
-
|
24 |
-
#fileInfo {
|
25 |
-
margin-top: 10px;
|
26 |
-
font-style: italic;
|
27 |
-
color: #666;
|
28 |
-
}
|
29 |
-
|
30 |
-
h1 {
|
31 |
-
color: #2c5282;
|
32 |
-
}
|
33 |
-
|
34 |
-
h2 {
|
35 |
-
color: #2a4365;
|
36 |
-
margin-top: 40px;
|
37 |
-
padding-bottom: 8px;
|
38 |
-
border-bottom: 1px solid #eaeaea;
|
39 |
-
}
|
40 |
-
|
41 |
-
h3 {
|
42 |
-
color: #2c5282;
|
43 |
-
margin-top: 25px;
|
44 |
-
}
|
45 |
-
|
46 |
-
#document-container {
|
47 |
-
background-color: white;
|
48 |
-
padding: 20px;
|
49 |
-
border: 1px solid #ddd;
|
50 |
-
border-radius: 5px;
|
51 |
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
52 |
-
}
|
53 |
-
|
54 |
-
.section {
|
55 |
-
margin-bottom: 20px;
|
56 |
-
}
|
57 |
-
|
58 |
-
.subsection {
|
59 |
-
margin-left: 15px;
|
60 |
-
}
|
61 |
-
|
62 |
-
table {
|
63 |
-
width: 100%;
|
64 |
-
border-collapse: collapse;
|
65 |
-
margin: 15px 0;
|
66 |
-
}
|
67 |
-
|
68 |
-
table,
|
69 |
-
th,
|
70 |
-
td {
|
71 |
-
border: 1px solid #ddd;
|
72 |
-
}
|
73 |
-
|
74 |
-
th,
|
75 |
-
td {
|
76 |
-
padding: 12px;
|
77 |
-
text-align: left;
|
78 |
-
}
|
79 |
-
|
80 |
-
th {
|
81 |
-
background-color: #f2f2f2;
|
82 |
-
}
|
83 |
-
|
84 |
-
pre {
|
85 |
-
background-color: #f8f8f8;
|
86 |
-
padding: 10px;
|
87 |
-
border-radius: 5px;
|
88 |
-
overflow-x: auto;
|
89 |
-
font-family: Consolas, monospace;
|
90 |
-
}
|
91 |
-
|
92 |
-
.loading {
|
93 |
-
text-align: center;
|
94 |
-
padding: 40px;
|
95 |
-
font-size: 20px;
|
96 |
-
color: #666;
|
97 |
-
}
|
98 |
-
|
99 |
-
.error {
|
100 |
-
color: #e53e3e;
|
101 |
-
padding: 10px;
|
102 |
-
background-color: #fff5f5;
|
103 |
-
border-left: 4px solid #e53e3e;
|
104 |
-
margin: 15px 0;
|
105 |
-
}
|
106 |
-
|
107 |
-
#specInput, #versionInput {
|
108 |
-
padding: 10px;
|
109 |
-
border: 1px solid #ddd;
|
110 |
-
border-radius: 4px;
|
111 |
-
}
|
112 |
-
|
113 |
-
#fileInput {
|
114 |
-
padding: 10px;
|
115 |
-
border: 1px solid #ddd;
|
116 |
-
border-radius: 4px;
|
117 |
-
width: 100%;
|
118 |
-
max-width: 400px;
|
119 |
-
}
|
120 |
-
|
121 |
-
button {
|
122 |
-
background-color: #3182ce;
|
123 |
-
color: white;
|
124 |
-
border: none;
|
125 |
-
padding: 10px 15px;
|
126 |
-
border-radius: 4px;
|
127 |
-
cursor: pointer;
|
128 |
-
transition: background-color 0.3s;
|
129 |
-
}
|
130 |
-
|
131 |
-
button:hover {
|
132 |
-
background-color: #2c5282;
|
133 |
-
}
|
134 |
-
|
135 |
-
button.active {
|
136 |
-
background-color: #223c5d;
|
137 |
-
}
|
138 |
-
|
139 |
-
.tabcontent {
|
140 |
-
display: none;
|
141 |
-
}
|
142 |
-
|
143 |
-
@media print {
|
144 |
-
.file-input-container {
|
145 |
-
display: none;
|
146 |
-
}
|
147 |
-
|
148 |
-
body {
|
149 |
-
padding: 0;
|
150 |
-
max-width: none;
|
151 |
-
}
|
152 |
-
|
153 |
-
#document-container {
|
154 |
-
border: none;
|
155 |
-
box-shadow: none;
|
156 |
-
padding: 0;
|
157 |
-
}
|
158 |
-
}
|
159 |
-
|
160 |
-
/* Styles pour meilleure lisibilité des tableaux et contenus techniques */
|
161 |
-
.byte-table td {
|
162 |
-
font-family: Consolas, monospace;
|
163 |
-
font-size: 14px;
|
164 |
-
}
|
165 |
-
|
166 |
-
.code-block {
|
167 |
-
font-family: Consolas, monospace;
|
168 |
-
background-color: #f8f8f8;
|
169 |
-
padding: 2px 4px;
|
170 |
-
border-radius: 3px;
|
171 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/index.html
DELETED
@@ -1,42 +0,0 @@
|
|
1 |
-
<!DOCTYPE html>
|
2 |
-
<html lang="fr">
|
3 |
-
|
4 |
-
<head>
|
5 |
-
<meta charset="UTF-8">
|
6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
-
<title>3GPP Specification Visualizor</title>
|
8 |
-
<link rel="stylesheet" href="static/style.css">
|
9 |
-
</head>
|
10 |
-
|
11 |
-
<body>
|
12 |
-
<div class="header">
|
13 |
-
<h1>Visualiseur de Spécifications 3GPP</h1>
|
14 |
-
<div class="tabs">
|
15 |
-
<button class="tablinks active" onclick="openTab(event, 'tab1')" data-loading="Veuillez charger un fichier de spécification 3GPP">Fichier JSON</button>
|
16 |
-
<button class="tablinks" onclick="openTab(event, 'tab2')" data-loading="Veuillez chercher une spécification 3GPP">Spécification</button>
|
17 |
-
</div>
|
18 |
-
<div id="tab1" class="tabcontent">
|
19 |
-
<div class="file-input-container"> <input type="file" id="fileInput" accept=".json">
|
20 |
-
<div id="fileInfo"></div>
|
21 |
-
</div>
|
22 |
-
<div id="document-container">
|
23 |
-
|
24 |
-
</div>
|
25 |
-
</div>
|
26 |
-
<div id="tab2" class="tabcontent">
|
27 |
-
<div class="file-input-container">
|
28 |
-
<label for="specInput">Spécification N°</label>
|
29 |
-
<input type="text" id="specInput" style="width: 45px;">
|
30 |
-
<label for="versionInput">Version <input type="checkbox" id="versCheck"></label>
|
31 |
-
<input type="text" id="versionInput" style="width: 45px;" disabled>
|
32 |
-
<button onclick="handleSpecSearch()">Afficher</button>
|
33 |
-
</div>
|
34 |
-
<div id="document-container">
|
35 |
-
|
36 |
-
</div>
|
37 |
-
</div>
|
38 |
-
</div>
|
39 |
-
<script src="static/script.js"></script>
|
40 |
-
</body>
|
41 |
-
|
42 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|