66from importlib .resources import files
77from pathlib import Path
88
9- import markdown
109import pyprojroot
1110from converter_app .profile_migration .utils .registration import Migrations
1211from converter_app .validation import validate_profile
1312
14- from mdutils .mdutils import MdUtils # https://github.com/didix21/mdutils
15-
1613from profile_manager .parse_ast import read_metadata_from_readercode
1714
1815program_name = "Chemotion Converter"
@@ -48,13 +45,6 @@ def build_index():
4845 docs_reader_dir = Path (base_path , "docs" , "atch" , "server" , "readers" )
4946 os .makedirs (docs_profile_dir , exist_ok = True )
5047 os .makedirs (docs_reader_dir , exist_ok = True )
51- md_file = MdUtils (file_name = 'index' , title = program_name )
52- # Additional Markdown syntax...
53- md_file .new_paragraph (f"{ program_name } is a very powerful python file converter "
54- f"running as a stand-alone flask server or included in an ELN or scientific Repository "
55- f"(like the { md_file .new_inline_link (link = "https://chemotion.net/" , text = "Chemotion ELN" )} , other ELNs we cannot guarantee) "
56- f"and called via API during file upload. "
57- f"For local and offline users, it is also possible to use it as an CLI tool." )
5848
5949 reader_dir = files ("converter_app" ) / "readers"
6050
@@ -74,7 +64,6 @@ def build_index():
7464 shutil .copyfileobj (
7565 cast (BinaryIO , source ), cast (BinaryIO , dest )
7666 )
77- reader_link = f"<a href=\" atch/server/readers/{ reader .name } \" download>{ reader .name } </a>"
7867
7968 reader_entry = {
8069 "class name" : my_ast [0 ],
@@ -83,24 +72,18 @@ def build_index():
8372 "check" : my_ast [3 ].strip () if my_ast [3 ] else "" ,
8473 }
8574
86- readers_dict [reader_link ] = reader_entry
75+ readers_dict [reader . name ] = reader_entry
8776
8877 except Exception as e :
8978 print (f"Skipping { reader .name } : { e } " )
9079 continue
9180
92- md_file .new_header (level = 1 , title = 'Readers' )
93-
94- md_file .new_paragraph ("A reader is a python class file handling the translation of your input file format to a usable python object."
95- " It is created by providing an example file to the developers or python coders and used and defined by the " +
96- md_file .new_inline_link (link = "https://github.com/ComPlat/chemotion-converter-app" ,
97- text = "converter app backend" ) + "." )
98-
9981 table_header = ["file name (click to download from this GitHub.io mirror)" ]
10082 if reader_entry :
10183 table_header += list (reader_entry .keys ())
102- dict_to_md_table (md_file , table_header , readers_dict )
10384
85+ readers_row_data , readers_column_defs = readers_dict_to_grid_config ()
86+ readers_table = dict_to_ag_grid_html (readers_row_data , readers_column_defs , "readers" )
10487
10588 for profile in profile_dir .glob ("*.json" ):
10689 with open (profile , "r" ) as file :
@@ -130,88 +113,136 @@ def build_index():
130113
131114 # Copy profile JSON to docs and link to the local docs path
132115 shutil .copy2 (profile , docs_profile_dir / profile .name )
133- profile_link = (
134- f"<a href=\" atch/server/profiles/{ profile_id } .json\" download>{ profile_id } </a>"
135- )
136- profiles_dict [profile_link ] = profile_entry
137-
138- md_file .new_header (level = 1 , title = 'Profiles' )
139-
140- md_file .new_paragraph ("A profile is JSON file defining a ruleset on how to convert your input file."
141- " Normally, it is created by uploading an example of your input file to the GUI of the " +
142- md_file .new_inline_link (link = "https://github.com/ComPlat/chemotion-converter-client" , text = "converter client frontend" ) + "." )
116+ profiles_dict [profile_id ] = profile_entry
143117
144118 table_header = ["id (click to download from this GitHub.io mirror)" ] + list (profile_entry .keys ())
145119 profiles_sorted = dict (sorted (
146120 profiles_dict .items (),
147121 key = lambda item : (item [1 ].get ("extension" ) or "" ).lower (),
148122 ))
149- dict_to_md_table (md_file , table_header , profiles_sorted )
150-
151- md_file .create_md_file ()
123+ profiles_row_data , profiles_column_defs = profiles_dict_to_grid_config ()
124+ profiles_table = dict_to_ag_grid_html (profiles_row_data , profiles_column_defs , "profiles" )
152125
153126 template_path = Path (__file__ ).parent .joinpath ("index_template.html" )
154- fill_md_into_html ( md_file , template_path )
127+ fill_data_into_html ( template_path , readers_table , profiles_table )
155128
156129
130+ def readers_dict_to_grid_config ():
131+ row_data = [
132+ {"file name" : k , ** v }
133+ for k , v in readers_dict .items ()
134+ ]
157135
158- def fill_md_into_html (md_file : MdUtils , html_file : Path ):
136+ column_defs = [
137+ {"field" : "file name" , "pinned" : "left" , "cellRenderer" : "linkRenderer" },
138+ * [
139+ {
140+ "field" : key ,
141+ ** ({"cellRenderer" : "codeCellRenderer" , "flex" : 3 } if key == "check" else {})
142+ }
143+ for key in next (iter (readers_dict .values ()))
144+ ],
145+ ]
146+ return row_data , column_defs
147+
148+ def profiles_dict_to_grid_config ():
149+ row_data = [
150+ {"id" : k , ** v }
151+ for k , v in profiles_dict .items ()
152+ ]
153+ column_defs = [
154+ {"field" : "id" , "pinned" : "left" , "cellRenderer" : "linkRenderer" },
155+ * [
156+ {
157+ "field" : key ,
158+ ** ({"valueFormatter" : "value && value.map(v => `${v[0]}: ${v[1]}`).join(', ')" }
159+ if key in ["identifiers" , "software" , "devices" ] else {}
160+ )
161+ }
162+ for key in next (iter (profiles_dict .values ()))
163+ ],
164+ ]
165+ return row_data , column_defs
166+
167+
168+ def dict_to_ag_grid_html (row_data , column_defs , dict_type ):
169+ grid_id = f"""{ dict_type } Grid"""
170+
171+ return f"""<div id="{ grid_id } " class="ag-theme-alpine" style="height: 400px; width: 100%;"></div>
172+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ag-grid-community/styles/ag-grid.css">
173+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ag-grid-community/styles/ag-theme-alpine.css">
174+ <script src="https://cdn.jsdelivr.net/npm/ag-grid-community/dist/ag-grid-community.min.js"></script>
175+
176+ <script>
177+ document.addEventListener("DOMContentLoaded", function () {{
178+
179+ function codeCellRenderer(params) {{
180+ if (!params.value) return "";
181+
182+ return `
183+ <code style="
184+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
185+ white-space: pre-wrap;
186+ display: block;
187+ font-size: 12px;
188+ background: #f6f8fa;
189+ padding: 6px 8px;
190+ border-radius: 6px;
191+ line-height: 1.4;
192+ ">${{params.value}}</code>
193+ `;
194+ }}
195+
196+ function linkRenderer(params) {{
197+ if (!params.value) return "";
198+ return `<a href="atch/server/{ dict_type } /${{params.value}}{ '.json' if dict_type == 'profiles' else '' } " download
199+ target="_blank"
200+ rel="noopener"
201+ >
202+ ${{params.value}}
203+ </a>`;
204+ }}
205+
206+ const gridOptions = {{
207+ theme: "legacy",
208+ columnDefs: { json .dumps (column_defs )} ,
209+ rowData: { json .dumps (row_data )} ,
210+ defaultColDef: {{
211+ flex: 1,
212+ sortable: true,
213+ filter: true,
214+ resizable: true,
215+ wrapText: true,
216+ autoHeight: true
217+ }},
218+ components: {{
219+ codeCellRenderer: codeCellRenderer,
220+ linkRenderer: linkRenderer,
221+ }}
222+ }};
223+
224+ agGrid.createGrid(
225+ document.getElementById("{ grid_id } "),
226+ gridOptions
227+ );
228+ }});
229+ </script>
230+ """
231+
232+
233+
234+ def fill_data_into_html (html_file : Path , readers_table , profiles_table ):
159235 with open (html_file , "r" ) as file :
160236 html_content = file .read ()
161- markdown_content = markdown .markdown (md_file .file_data_text , extensions = ["tables" , "fenced_code" ])
162237 html_content = html_content .replace ("{{ PROGRAM_NAME }}" , program_name )
163- html_content = html_content .replace ("{{ TABLE_CONTENT }}" , markdown_content )
238+ html_content = html_content .replace ("{{ READERS_TABLE }}" , readers_table )
239+ html_content = html_content .replace ("{{ PROFILES_TABLE }}" , profiles_table )
164240 base_path = pyprojroot .find_root (pyprojroot .has_dir ("build" ))
165241 os .makedirs (os .path .join (base_path , "docs" ), exist_ok = True )
166242 index_path = Path (base_path , "docs" , "index.html" )
167243 with open (index_path , "w" ) as file :
168244 file .write (html_content )
169245
170- def convert_docs_md_to_html ():
171- docs_dir = Path (__file__ ).parent .parent .joinpath ("docs" )
172- if not docs_dir .exists ():
173- return
174- for md_file in docs_dir .glob ("*.md" ):
175- with open (md_file , "r" ) as file :
176- md_text = file .read ()
177- html_body = markdown .markdown (md_text , extensions = ["tables" , "fenced_code" ])
178- html_title = md_file .stem .replace ("_" , " " ).title ()
179- html_text = (
180- "<!doctype html>\n "
181- "<html lang=\" en\" >\n "
182- "<head>\n "
183- " <meta charset=\" utf-8\" >\n "
184- f" <title>{ html_title } </title>\n "
185- " <meta name=\" viewport\" content=\" width=device-width, initial-scale=1\" >\n "
186- " <style>\n "
187- " body { font-family: Arial, sans-serif; margin: 2rem; line-height: 1.6; }\n "
188- " pre { overflow-x: auto; }\n "
189- " code { font-family: \" Courier New\" , monospace; }\n "
190- " table { border-collapse: collapse; }\n "
191- " th, td { border: 1px solid #ccc; padding: 0.4rem 0.6rem; }\n "
192- " </style>\n "
193- "</head>\n "
194- "<body>\n "
195- f"{ html_body } \n "
196- "</body>\n "
197- "</html>\n "
198- )
199- html_path = md_file .with_suffix (".html" )
200- with open (html_path , "w" ) as file :
201- file .write (html_text )
202-
203- def dict_to_md_table (md_file , table_header , dict_to_write ):
204- table_content = []
205- for key in dict_to_write :
206- row = [key ] + [clean_value (value ) for value in dict_to_write [key ].values ()]
207- table_content .append (row )
208- table = [table_header ] + table_content
209- # Flatten for Markdown or similar tools
210- flat_table = [cell for row in table for cell in row ]
211- md_file .new_line ()
212- md_file .new_table (columns = len (table_header ), rows = int (len (flat_table ) / len (table_header )), text = flat_table )
213-
214-
215246def validate_profiles ():
216247 profile_dir = Path (__file__ ).parent .parent .joinpath ('profiles/public' )
217248 for profile in profile_dir .glob ("*.json" ):
@@ -229,5 +260,4 @@ def migrate_profiles():
229260 if len (sysargs ) >= 2 :
230261 if sys .argv [1 ] == 'build_index' :
231262 build_index ()
232- convert_docs_md_to_html ()
233263 print ("EOC reached" )
0 commit comments