Documentation: - Add ShopDB-API.md with full API reference (all GET/POST endpoints) - Add detailed docs for Update-ShopfloorPCs-Remote, Invoke-RemoteMaintenance, Update-PC-CompleteAsset - Add DATA_COLLECTION_PARITY.md comparing local vs remote data collection - Add HTML versions of all documentation with styled code blocks - Document software deployment mechanism and how to add new apps - Document deprecated scripts (Invoke-RemoteAssetCollection, Install-KioskApp) Script Updates: - Update deployment source paths to network share (tsgwp00525.wjs.geaerospace.net) - InstallDashboard: \\...\scripts\Dashboard\GEAerospaceDashboardSetup.exe - InstallLobbyDisplay: \\...\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe - UpdateEMxAuthToken: \\...\scripts\eMx\eMxInfo.txt - DeployUDCWebServerConfig: \\...\scripts\UDC\udc_webserver_settings.json - Update machine network detection to include 100.0.0.* for CMM cases - Rename PC Type #9 from "Part Marker" to "Inspection" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
412 lines
13 KiB
Python
412 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Convert Markdown documentation to styled HTML
|
|
"""
|
|
|
|
import re
|
|
import os
|
|
import html
|
|
|
|
def convert_md_to_html(md_content, title="Documentation"):
|
|
"""Convert markdown content to styled HTML."""
|
|
|
|
# HTML template with CSS styling
|
|
html_template = '''<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{title}</title>
|
|
<style>
|
|
* {{
|
|
box-sizing: border-box;
|
|
}}
|
|
body {{
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
line-height: 1.6;
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
padding: 20px 40px;
|
|
background-color: #ffffff;
|
|
color: #333;
|
|
}}
|
|
h1 {{
|
|
color: #1a5276;
|
|
border-bottom: 3px solid #1a5276;
|
|
padding-bottom: 10px;
|
|
margin-top: 0;
|
|
}}
|
|
h2 {{
|
|
color: #2874a6;
|
|
border-bottom: 2px solid #d5dbdb;
|
|
padding-bottom: 8px;
|
|
margin-top: 40px;
|
|
}}
|
|
h3 {{
|
|
color: #2e86ab;
|
|
margin-top: 30px;
|
|
}}
|
|
h4 {{
|
|
color: #5d6d7e;
|
|
margin-top: 25px;
|
|
}}
|
|
a {{
|
|
color: #2980b9;
|
|
text-decoration: none;
|
|
}}
|
|
a:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
code {{
|
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
background-color: #f4f4f4;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
font-size: 0.9em;
|
|
border: 1px solid #e1e1e1;
|
|
}}
|
|
pre {{
|
|
background-color: #2d2d2d;
|
|
color: #f8f8f2;
|
|
padding: 15px 20px;
|
|
border-radius: 6px;
|
|
overflow-x: auto;
|
|
font-family: 'Cascadia Mono', 'JetBrains Mono', 'Fira Code', 'Source Code Pro', 'DejaVu Sans Mono', 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
border: 1px solid #444;
|
|
margin: 15px 0;
|
|
-webkit-font-feature-settings: "liga" 0;
|
|
font-feature-settings: "liga" 0;
|
|
letter-spacing: 0;
|
|
}}
|
|
pre code {{
|
|
background-color: transparent;
|
|
padding: 0;
|
|
border: none;
|
|
color: inherit;
|
|
font-size: inherit;
|
|
}}
|
|
table {{
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
margin: 15px 0;
|
|
font-size: 14px;
|
|
}}
|
|
th, td {{
|
|
border: 1px solid #ddd;
|
|
padding: 10px 12px;
|
|
text-align: left;
|
|
}}
|
|
th {{
|
|
background-color: #34495e;
|
|
color: white;
|
|
font-weight: 600;
|
|
}}
|
|
tr:nth-child(even) {{
|
|
background-color: #f9f9f9;
|
|
}}
|
|
tr:hover {{
|
|
background-color: #f1f1f1;
|
|
}}
|
|
ul, ol {{
|
|
padding-left: 25px;
|
|
}}
|
|
li {{
|
|
margin-bottom: 5px;
|
|
}}
|
|
blockquote {{
|
|
border-left: 4px solid #3498db;
|
|
margin: 15px 0;
|
|
padding: 10px 20px;
|
|
background-color: #f8f9fa;
|
|
color: #555;
|
|
}}
|
|
hr {{
|
|
border: none;
|
|
border-top: 2px solid #eee;
|
|
margin: 30px 0;
|
|
}}
|
|
.toc {{
|
|
background-color: #f8f9fa;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 6px;
|
|
padding: 20px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
.toc h2 {{
|
|
margin-top: 0;
|
|
border-bottom: none;
|
|
font-size: 1.2em;
|
|
}}
|
|
.toc ul {{
|
|
list-style-type: none;
|
|
padding-left: 0;
|
|
}}
|
|
.toc li {{
|
|
margin-bottom: 8px;
|
|
}}
|
|
.toc a {{
|
|
color: #2c3e50;
|
|
}}
|
|
.note {{
|
|
background-color: #fff3cd;
|
|
border-left: 4px solid #ffc107;
|
|
padding: 10px 15px;
|
|
margin: 15px 0;
|
|
}}
|
|
.warning {{
|
|
background-color: #f8d7da;
|
|
border-left: 4px solid #dc3545;
|
|
padding: 10px 15px;
|
|
margin: 15px 0;
|
|
}}
|
|
@media print {{
|
|
body {{
|
|
max-width: 100%;
|
|
padding: 20px;
|
|
}}
|
|
pre {{
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
}}
|
|
h2 {{
|
|
page-break-before: auto;
|
|
}}
|
|
pre, table {{
|
|
page-break-inside: avoid;
|
|
}}
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{content}
|
|
</body>
|
|
</html>'''
|
|
|
|
lines = md_content.split('\n')
|
|
html_lines = []
|
|
i = 0
|
|
in_list = False
|
|
list_type = None
|
|
|
|
while i < len(lines):
|
|
line = lines[i]
|
|
|
|
# Empty line - close any open list
|
|
if not line.strip():
|
|
if in_list:
|
|
html_lines.append(f'</{list_type}>')
|
|
in_list = False
|
|
list_type = None
|
|
i += 1
|
|
continue
|
|
|
|
# Headers
|
|
if line.startswith('# '):
|
|
if in_list:
|
|
html_lines.append(f'</{list_type}>')
|
|
in_list = False
|
|
text = process_inline(line[2:].strip())
|
|
anchor = slugify(line[2:].strip())
|
|
html_lines.append(f'<h1 id="{anchor}">{text}</h1>')
|
|
i += 1
|
|
elif line.startswith('## '):
|
|
if in_list:
|
|
html_lines.append(f'</{list_type}>')
|
|
in_list = False
|
|
text = process_inline(line[3:].strip())
|
|
anchor = slugify(line[3:].strip())
|
|
html_lines.append(f'<h2 id="{anchor}">{text}</h2>')
|
|
i += 1
|
|
elif line.startswith('### '):
|
|
if in_list:
|
|
html_lines.append(f'</{list_type}>')
|
|
in_list = False
|
|
text = process_inline(line[4:].strip())
|
|
anchor = slugify(line[4:].strip())
|
|
html_lines.append(f'<h3 id="{anchor}">{text}</h3>')
|
|
i += 1
|
|
elif line.startswith('#### '):
|
|
if in_list:
|
|
html_lines.append(f'</{list_type}>')
|
|
in_list = False
|
|
text = process_inline(line[5:].strip())
|
|
anchor = slugify(line[5:].strip())
|
|
html_lines.append(f'<h4 id="{anchor}">{text}</h4>')
|
|
i += 1
|
|
|
|
# Horizontal rule
|
|
elif line.strip() == '---':
|
|
if in_list:
|
|
html_lines.append(f'</{list_type}>')
|
|
in_list = False
|
|
html_lines.append('<hr>')
|
|
i += 1
|
|
|
|
# Code blocks
|
|
elif line.strip().startswith('```'):
|
|
if in_list:
|
|
html_lines.append(f'</{list_type}>')
|
|
in_list = False
|
|
lang = line.strip()[3:]
|
|
code_lines = []
|
|
i += 1
|
|
while i < len(lines) and not lines[i].strip().startswith('```'):
|
|
code_lines.append(html.escape(lines[i]))
|
|
i += 1
|
|
code_content = '\n'.join(code_lines)
|
|
if lang:
|
|
html_lines.append(f'<pre><code class="language-{lang}">{code_content}</code></pre>')
|
|
else:
|
|
html_lines.append(f'<pre><code>{code_content}</code></pre>')
|
|
i += 1 # Skip closing ```
|
|
|
|
# Tables
|
|
elif '|' in line and i + 1 < len(lines) and '---' in lines[i + 1]:
|
|
if in_list:
|
|
html_lines.append(f'</{list_type}>')
|
|
in_list = False
|
|
html_lines.append('<table>')
|
|
# Header row
|
|
cells = [c.strip() for c in line.split('|')[1:-1]]
|
|
html_lines.append('<thead><tr>')
|
|
for cell in cells:
|
|
html_lines.append(f'<th>{process_inline(cell)}</th>')
|
|
html_lines.append('</tr></thead>')
|
|
i += 2 # Skip header and separator
|
|
html_lines.append('<tbody>')
|
|
while i < len(lines) and '|' in lines[i]:
|
|
cells = [c.strip() for c in lines[i].split('|')[1:-1]]
|
|
html_lines.append('<tr>')
|
|
for cell in cells:
|
|
html_lines.append(f'<td>{process_inline(cell)}</td>')
|
|
html_lines.append('</tr>')
|
|
i += 1
|
|
html_lines.append('</tbody></table>')
|
|
|
|
# Bullet lists
|
|
elif line.strip().startswith('- ') or line.strip().startswith('* '):
|
|
if not in_list or list_type != 'ul':
|
|
if in_list:
|
|
html_lines.append(f'</{list_type}>')
|
|
html_lines.append('<ul>')
|
|
in_list = True
|
|
list_type = 'ul'
|
|
text = process_inline(line.strip()[2:])
|
|
html_lines.append(f'<li>{text}</li>')
|
|
i += 1
|
|
|
|
# Numbered lists
|
|
elif re.match(r'^\d+\.\s', line.strip()):
|
|
if not in_list or list_type != 'ol':
|
|
if in_list:
|
|
html_lines.append(f'</{list_type}>')
|
|
html_lines.append('<ol>')
|
|
in_list = True
|
|
list_type = 'ol'
|
|
text = process_inline(re.sub(r'^\d+\.\s', '', line.strip()))
|
|
html_lines.append(f'<li>{text}</li>')
|
|
i += 1
|
|
|
|
# Blockquote
|
|
elif line.strip().startswith('>'):
|
|
if in_list:
|
|
html_lines.append(f'</{list_type}>')
|
|
in_list = False
|
|
text = process_inline(line.strip()[1:].strip())
|
|
html_lines.append(f'<blockquote>{text}</blockquote>')
|
|
i += 1
|
|
|
|
# Regular paragraph
|
|
else:
|
|
if in_list:
|
|
html_lines.append(f'</{list_type}>')
|
|
in_list = False
|
|
para_lines = [line.strip()]
|
|
i += 1
|
|
while i < len(lines) and lines[i].strip() and not lines[i].startswith('#') and not lines[i].startswith('```') and not lines[i].strip().startswith('- ') and not lines[i].strip().startswith('* ') and '|' not in lines[i] and not re.match(r'^\d+\.\s', lines[i].strip()) and lines[i].strip() != '---':
|
|
para_lines.append(lines[i].strip())
|
|
i += 1
|
|
text = process_inline(' '.join(para_lines))
|
|
html_lines.append(f'<p>{text}</p>')
|
|
|
|
# Close any remaining list
|
|
if in_list:
|
|
html_lines.append(f'</{list_type}>')
|
|
|
|
content = '\n'.join(html_lines)
|
|
return html_template.format(title=html.escape(title), content=content)
|
|
|
|
def process_inline(text):
|
|
"""Process inline markdown formatting."""
|
|
# Escape HTML first
|
|
# But we need to be careful not to double-escape
|
|
|
|
# Bold
|
|
text = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', text)
|
|
|
|
# Italic
|
|
text = re.sub(r'\*([^*]+)\*', r'<em>\1</em>', text)
|
|
|
|
# Inline code (before links to avoid conflicts)
|
|
text = re.sub(r'`([^`]+)`', lambda m: f'<code>{html.escape(m.group(1))}</code>', text)
|
|
|
|
# Links
|
|
text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'<a href="\2">\1</a>', text)
|
|
|
|
# Checkmarks and X marks
|
|
text = text.replace('✓', '✓')
|
|
text = text.replace('✗', '✗')
|
|
|
|
return text
|
|
|
|
def slugify(text):
|
|
"""Convert text to URL-friendly slug."""
|
|
text = text.lower()
|
|
text = re.sub(r'[^a-z0-9\s-]', '', text)
|
|
text = re.sub(r'[\s]+', '-', text)
|
|
return text
|
|
|
|
def convert_file(md_path, html_path):
|
|
"""Convert a markdown file to HTML."""
|
|
print(f"Converting {os.path.basename(md_path)} -> {os.path.basename(html_path)}")
|
|
|
|
with open(md_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# Extract title from first h1
|
|
title_match = re.search(r'^# (.+)$', content, re.MULTILINE)
|
|
title = title_match.group(1) if title_match else os.path.basename(md_path)
|
|
|
|
html_content = convert_md_to_html(content, title)
|
|
|
|
with open(html_path, 'w', encoding='utf-8') as f:
|
|
f.write(html_content)
|
|
|
|
def main():
|
|
docs_dir = '/home/camp/projects/powershell/docs'
|
|
|
|
md_files = [
|
|
'Update-ShopfloorPCs-Remote.md',
|
|
'Invoke-RemoteMaintenance.md',
|
|
'Update-PC-CompleteAsset.md',
|
|
'DATA_COLLECTION_PARITY.md',
|
|
'ShopDB-API.md'
|
|
]
|
|
|
|
for md_file in md_files:
|
|
md_path = os.path.join(docs_dir, md_file)
|
|
html_path = os.path.join(docs_dir, md_file.replace('.md', '.html'))
|
|
|
|
if os.path.exists(md_path):
|
|
convert_file(md_path, html_path)
|
|
else:
|
|
print(f"Warning: {md_path} not found")
|
|
|
|
print("\nConversion complete!")
|
|
print(f"HTML files saved to: {docs_dir}")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|