Add fixnetworkshare, winrm-setup-package, udc remote-execution suites

- NetworkDriveManager.ps1: S: drive repair utility
- winrm-setup-package: Invoke-RemoteTask helper + Setup-WinRM.bat + HTML guide
- remote-execution/udc: UDC_Update.ps1 and batch wrappers for updating
  DNC controllers on shop-floor PCs
- Invoke-RemoteMaintenance.ps1: substantial rework (~1650 lines)
- Schedule-Maintenance and complete-asset minor updates
- Bump edncfix gitlink to v1.6.0 (2748bfa)
- .gitignore: block inventory.csv/xlsx (CUI) and logs_*.txt (per-host logs)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-04-17 12:04:40 -04:00
parent 847ec402bd
commit 86b32d8597
24 changed files with 6945 additions and 1352 deletions

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,7 @@ Remote maintenance toolkit for executing maintenance tasks on shopfloor PCs via
## Table of Contents
- [Overview](#overview)
- [First-Time Setup](#first-time-setup)
- [API Integration](#api-integration)
- [Prerequisites](#prerequisites)
- [Quick Start](#quick-start)
@@ -17,6 +18,8 @@ Remote maintenance toolkit for executing maintenance tasks on shopfloor PCs via
- [Service Management](#how-to-manage-services)
- [Time Synchronization](#how-to-fix-time-sync-issues)
- [DNC Configuration](#how-to-update-dnc-configurations)
- [File Deployment](#how-to-deploy-files)
- [Registry Import](#how-to-import-registry-files)
- [Software Deployment](#how-to-deploy-software)
- [Batch Operations](#how-to-run-batch-operations)
- [Targeting Strategies](#targeting-strategies)
@@ -32,13 +35,53 @@ This script provides a comprehensive remote maintenance toolkit for managing sho
**Location:** `S:\dt\shopfloor\scripts\remote-execution\Invoke-RemoteMaintenance.ps1`
**Key Features:**
- 19 maintenance tasks available
- 22 maintenance tasks available
- General-purpose file deployment (`CopyFile`) and registry import (`ImportReg`) tasks
- Registry imports run as logged-in user via scheduled task (HKCU support)
- Optional post-copy commands via scheduled task (service/app restarts)
- Multiple targeting options (by name, type, business unit, or all)
- Concurrent execution with configurable throttling
- Integration with ShopDB for PC discovery
---
## First-Time Setup
Before running this script for the first time, you must allow PowerShell script execution and unblock the script file.
### Step 1: Allow PowerShell Script Execution
Open PowerShell **as Administrator** and run:
```powershell
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
```
When prompted, type `Y` and press Enter. This allows locally-created scripts to run and requires downloaded scripts to be signed or unblocked.
> **Note:** You only need to do this once per user account. `RemoteSigned` is the recommended policy — it allows local scripts while still protecting against untrusted downloads.
### Step 2: Unblock the Script File
Files downloaded from a network share or the internet are marked as "blocked" by Windows. You must unblock the script before it can run.
**Option A — File Explorer (GUI):**
1. Navigate to `S:\dt\shopfloor\scripts\remote-execution\`
2. Right-click `Invoke-RemoteMaintenance.ps1` and select **Properties**
3. At the bottom of the General tab, check the **Unblock** checkbox
4. Click **Apply**, then **OK**
**Option B — PowerShell:**
```powershell
Unblock-File -Path "S:\dt\shopfloor\scripts\remote-execution\Invoke-RemoteMaintenance.ps1"
```
> **Important:** If you skip this step, you will get a security error when trying to run the script: *"cannot be loaded because running scripts is disabled on this system"* or *"cannot be loaded. The file is not digitally signed."*
---
## API Integration
When using `-All`, `-PcType`, or `-BusinessUnit` targeting, the script retrieves PC lists from the ShopDB API:
@@ -53,12 +96,12 @@ GET /api.asp?action=getShopfloorPCs&businessunitid=1 # Specific business unit
| ID | Type | ID | Type |
|----|------|----|------|
| 1 | Shopfloor | 7 | Heat Treat |
| 2 | CMM | 8 | Engineer |
| 3 | Wax Trace | 9 | Standard |
| 4 | Keyence | 10 | Inspection |
| 5 | EAS1000 | 11 | Dashboard |
| 6 | Genspect | 12 | Lobby Display |
| 1 | Standard | 7 | Keyence |
| 2 | Engineer | 8 | Genspect |
| 3 | Shopfloor | 9 | Heat Treat |
| 4 | Uncategorized | 10 | Inspection |
| 5 | CMM | 11 | Dashboard |
| 6 | Wax / Trace | 12 | Lobby Display |
**See:** [ShopDB API Reference](ShopDB-API.html) for complete API documentation.
@@ -133,6 +176,15 @@ The script outputs status for each PC:
|-----------|------|-------------|
| `-Task` | string | Maintenance task to execute |
### File Deployment Parameters (CopyFile / ImportReg)
| Parameter | Type | Description |
|-----------|------|-------------|
| `-SourcePath` | string | Source file path (local or UNC). Required for CopyFile and ImportReg |
| `-DestinationPath` | string | Destination file path on remote PCs. Required for CopyFile |
| `-RunCommand` | string | Command to run after CopyFile. Runs as logged-in user by default (via scheduled task) |
| `-AsSystem` | switch | Run ImportReg and -RunCommand in the WinRM session (SYSTEM context) instead of as logged-in user |
### Optional Parameters
| Parameter | Type | Default | Description |
@@ -140,7 +192,8 @@ The script outputs status for each PC:
| `-Credential` | PSCredential | Prompt | Remote authentication |
| `-ApiUrl` | string | Production | ShopDB API endpoint |
| `-ThrottleLimit` | int | 5 | Max concurrent sessions |
| `-DnsSuffix` | string | logon.ds.ge.com | DNS suffix for resolution |
| `-DnsSuffix` | string | logon.ds.ge.com | DNS suffix for FQDN resolution |
| `-LogFile` | switch | Off | Enable transcript logging to `logs/` directory |
### PC Types
@@ -194,13 +247,22 @@ TBD, Blisk, HPT, Spools, Inspection, Venture, Turn/Burn, DT
| Task | Description | Duration | Impact |
|------|-------------|----------|--------|
| `UpdateEMxAuthToken` | Update eMx auth from share | 1-2 min | None |
| `DeployUDCWebServerConfig` | Deploy UDC config | 1-2 min | None |
| `UpdateDNCMXHosts` | Update FtpHostPrimary/Secondary in DNC\MX registry | <1 min | None |
| `AuditDNCConfig` | Compare DNC registry vs UDC backup JSON, export CSV | 1-5 min | None |
| `CheckDefectTracker` | Check if Defect_Tracker.exe is running, export CSV | 1-5 min | None |
### File Deployment Tasks
| Task | Description | Duration | Impact |
|------|-------------|----------|--------|
| `CopyFile` | Copy file from `-SourcePath` to `-DestinationPath` on remote PCs | 1-2 min | Low |
| `ImportReg` | Copy `.reg` file and import via scheduled task as logged-in user | 1-2 min | Low |
### System Tasks
| Task | Description | Duration | Impact |
|------|-------------|----------|--------|
| `GPUpdate` | Force Group Policy refresh (`gpupdate /force`) | <1 min | Low |
| `Reboot` | Restart PC (30s delay) | 2-5 min | High |
### Software Deployment Tasks
@@ -222,18 +284,19 @@ Deployment tasks require source files to be available before execution:
| Task | Source File Path |
|------|------------------|
| `InstallDashboard` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\Dashboard\GEAerospaceDashboardSetup.exe` |
| `InstallLobbyDisplay` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe` |
| `UpdateEMxAuthToken` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\eMx\eMxInfo.txt` |
| `DeployUDCWebServerConfig` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\UDC\udc_webserver_settings.json` |
| `InstallDashboard` | `\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\Dashboard\GEAerospaceDashboardSetup.exe` |
| `InstallLobbyDisplay` | `\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe` |
| `CopyFile` | Any file - specified via `-SourcePath` parameter |
| `ImportReg` | Any `.reg` file - specified via `-SourcePath` parameter |
### How Deployment Works
1. **Pre-flight Check:** Script verifies source file exists
2. **WinRM Session:** Opens remote session to target PC
3. **File Push:** Copies source file to `C:\Windows\Temp\` on remote PC
4. **Execution:** Runs install/copy task using pushed file
5. **Cleanup:** Removes temp file from remote PC
4. **Execution:** Runs install/copy/import task using pushed file
5. **Post-action:** Optionally runs command as logged-in user via scheduled task
6. **Cleanup:** Removes temp file from remote PC
```
+---------------------+ WinRM +---------------------+
@@ -242,50 +305,44 @@ Deployment tasks require source files to be available before execution:
| Source Files: | Push File | Temp Location: |
| - Setup.exe | ------------> | C:\Windows\Temp |
| - config.json | | |
| - eMxInfo.txt | Execute | Final Location: |
| (network) | ------------> | C:\Program Files |
| - settings.reg | Execute | Final Location: |
| (any path/UNC) | ------------> | -DestinationPath |
| | | |
| | Sched. Task | Logged-in User: |
| | ------------> | - regedit /s |
| | | - RunCommand |
+---------------------+ +---------------------+
```
### Directory Structure
### CopyFile Details
Ensure your script directory contains the required files:
The `CopyFile` task:
```
S:\dt\shopfloor\scripts\remote-execution\
├── Invoke-RemoteMaintenance.ps1
├── GEAerospaceDashboardSetup.exe # For InstallDashboard
├── GEAerospaceLobbyDisplaySetup.exe # For InstallLobbyDisplay
└── udc_webserver_settings.json # For DeployUDCWebServerConfig
```
1. **Source:** Any file specified via `-SourcePath` (local or UNC path)
2. **Destination:** Specified via `-DestinationPath`
3. **Backup:** Existing file is backed up as `<name>-old-<timestamp>.<ext>`
4. **Post-action:** If `-RunCommand` is specified, runs as the logged-in user via a one-shot scheduled task (same pattern as Dashboard/Lobby kiosk relaunch)
### eMx Auth Token Details
### ImportReg Details
The `UpdateEMxAuthToken` task:
The `ImportReg` task:
1. **Source:** `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\eMx\eMxInfo.txt`
2. **Destinations:** (both paths if they exist)
- `C:\Program Files\GE Aircraft Engines\DNC\eMxInfo.txt`
- `C:\Program Files (x86)\GE Aircraft Engines\DNC\eMxInfo.txt`
3. **Backup:** Creates `eMxInfo-old-YYYYMMDD-HHMMSS.txt` before overwriting
4. **Post-action:** Restarts DNC service (`LDnc.exe`)
### UDC Web Server Config Details
The `DeployUDCWebServerConfig` task:
1. **Pre-check:** Verifies UDC is installed (`C:\Program Files\UDC` exists)
2. **Skip:** PCs without UDC are skipped (not counted as failures)
3. **Destination:** `C:\ProgramData\UDC\udc_webserver_settings.json`
4. **Backup:** Creates backup before overwriting
1. **Source:** `.reg` file specified via `-SourcePath`
2. **Import method:** `regedit.exe /s` via one-shot scheduled task as logged-in user
3. **HKCU support:** Runs as the logged-in user, so both HKLM and HKCU keys apply correctly
4. **Fallback:** If no user is logged in, runs `regedit.exe /s` directly (HKLM only)
5. **Cleanup:** Temp `.reg` file removed after import
### Dashboard/Lobby Display Install Details
Both kiosk app installers:
1. **Installer type:** Inno Setup (supports `/VERYSILENT`)
2. **Execution:** Silent install with no user prompts
3. **Cleanup:** Installer removed from temp after execution
2. **Pre-install:** Kills running Edge kiosk via `PrepareToInstall` in Inno Setup
3. **Execution:** Silent install with no user prompts (120-second timeout)
4. **Post-install:** Creates a one-shot scheduled task to relaunch Edge in kiosk mode as the logged-in user (e.g. `lg044513sd`), then auto-deletes the task
5. **Cleanup:** Installer removed from temp after execution
6. **Connectivity:** Offline PCs are skipped with a ping check before connecting
**Uninstall GUIDs:**
- Dashboard: `{9D9EEE25-4D24-422D-98AF-2ADEDA4745ED}`
@@ -295,7 +352,7 @@ Both kiosk app installers:
To add a new application for deployment, edit the script in two places:
**Step 1: Add to `$KioskAppConfig` hashtable (~line 1388)**
**Step 1: Add to `$KioskAppConfig` hashtable**
```powershell
$KioskAppConfig = @{
@@ -304,10 +361,11 @@ $KioskAppConfig = @{
# Add new application
'InstallNewApp' = @{
Action = 'Install'
InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\NewApp\NewAppSetup.exe'
InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\NewApp\NewAppSetup.exe'
InstallerName = 'NewAppSetup.exe'
AppName = 'New Application Name'
UninstallGuid = '{YOUR-GUID-HERE}' # Find in registry after manual install
KioskUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb/your-page/' # Optional: relaunch Edge kiosk after install
}
'UninstallNewApp' = @{
Action = 'Uninstall'
@@ -334,7 +392,7 @@ $KioskAppConfig = @{
**Step 3: Place installer on network share**
```
\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\NewApp\NewAppSetup.exe
\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\NewApp\NewAppSetup.exe
```
**Finding the Uninstall GUID:**
@@ -562,37 +620,129 @@ wuauclt /detectnow
### How to Update DNC Configurations
#### Update eMx Authentication Token
#### Update DNC MX Hosts
**Scenario:** eMx authentication is failing on DNC PCs.
**Scenario:** FtpHostPrimary/FtpHostSecondary in DNC\MX registry needs updating (hostname migration).
```powershell
# Single PC
.\Invoke-RemoteMaintenance.ps1 -ComputerName "DNC-PC01" -Task UpdateEMxAuthToken -Credential $cred
.\Invoke-RemoteMaintenance.ps1 -ComputerName "DNC-PC01" -Task UpdateDNCMXHosts -Credential $cred
# All shopfloor PCs
.\Invoke-RemoteMaintenance.ps1 -All -Task UpdateEMxAuthToken -Credential $cred
.\Invoke-RemoteMaintenance.ps1 -All -Task UpdateDNCMXHosts -Credential $cred
```
**What it does:**
1. Backs up existing `eMxInfo.txt` with timestamp
2. Copies new token file from network share
3. Verifies file was updated
1. Checks both 32-bit (WOW6432Node) and 64-bit registry paths
2. Only updates values matching the old hostname - skips unexpected values
3. Safe to run on all PCs: no-ops if DNC\MX key doesn't exist
#### Deploy UDC Web Server Configuration
#### Audit DNC Config vs UDC Backup
**Scenario:** UDC web server settings need to be updated.
**Scenario:** Verify DNC registry settings match UDC backup JSON files.
```powershell
# Deploy to PCs with UDC installed
.\Invoke-RemoteMaintenance.ps1 -ComputerName "UDC-PC01","UDC-PC02" -Task DeployUDCWebServerConfig -Credential $cred
.\Invoke-RemoteMaintenance.ps1 -All -Task AuditDNCConfig -Credential $cred -LogFile
```
**What it does:**
1. Checks if UDC is installed
2. Backs up existing configuration
3. Deploys new web server settings
4. Does NOT restart UDC (requires manual restart)
1. Reads DNC registry values (General, eFocas, Hssb, PPDCS keys)
2. Compares against UDC backup JSON files on the network share
3. Reports MATCH/MISMATCH/MISSING for each field
4. Exports CSV report to `logs/`
#### Check Defect Tracker Status
```powershell
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task CheckDefectTracker -Credential $cred
```
**What it does:**
1. Checks if `Defect_Tracker.exe` is running on each PC
2. Reports machine number + running status
3. Exports CSV report to `logs/`
---
### How to Deploy Files
**Scenario:** Push any file to remote PCs with automatic backup of existing files.
#### Basic File Copy
```powershell
# Copy a config file to a specific destination
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01","PC02" -Task CopyFile `
-SourcePath "\\server\share\config.json" `
-DestinationPath "C:\ProgramData\App\config.json" `
-Credential $cred
```
**What it does:**
1. Copies source file to `C:\Windows\Temp\` on remote PC via WinRM
2. Creates backup of existing file (if any) with timestamp
3. Moves temp file to final destination
4. Verifies deployment
#### File Copy with Post-Copy Command
**Scenario:** Deploy a file and restart a service/app in the logged-in user's session.
```powershell
# Deploy eMxInfo.txt and kill DNC so it picks up the new file
.\Invoke-RemoteMaintenance.ps1 -All -Task CopyFile `
-SourcePath "\\server\share\eMxInfo.txt" `
-DestinationPath "C:\Program Files (x86)\DNC\Server Files\eMxInfo.txt" `
-RunCommand "taskkill /IM DNCMain.exe /F" `
-Credential $cred
# Deploy UDC config (no restart needed)
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task CopyFile `
-SourcePath "\\server\share\udc_webserver_settings.json" `
-DestinationPath "C:\ProgramData\UDC\udc_webserver_settings.json" `
-Credential $cred
```
The `-RunCommand` runs via a one-shot scheduled task as the logged-in user, so it works for user-session processes (same pattern as Dashboard/Lobby Display kiosk relaunch).
---
### How to Import Registry Files
**Scenario:** Apply registry settings from a `.reg` file to remote PCs.
```powershell
# Import a .reg file on all shopfloor PCs
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task ImportReg `
-SourcePath "\\server\share\intranet-zone.reg" `
-Credential $cred
# Import on specific PCs
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task ImportReg `
-SourcePath "C:\Scripts\my-settings.reg" `
-Credential $cred
```
**What it does (default — as logged-in user):**
1. Copies `.reg` file to `C:\Windows\Temp\` on remote PC via WinRM
2. Creates a one-shot scheduled task as the logged-in user
3. Runs `regedit.exe /s` to silently import the registry file
4. Cleans up temp file and scheduled task
**HKCU support:** Because the import runs as the logged-in user (via scheduled task), both `HKLM` and `HKCU` keys in the `.reg` file are applied correctly. If no user is logged in, it falls back to direct import (HKLM only).
#### HKLM-Only / System Context
Use `-AsSystem` when the `.reg` file only contains `HKLM` keys and you want to skip the scheduled task overhead:
```powershell
# Import HKLM-only registry settings directly as SYSTEM
.\Invoke-RemoteMaintenance.ps1 -All -Task ImportReg `
-SourcePath "\\server\share\machine-policy.reg" `
-AsSystem -Credential $cred
```
This runs `regedit.exe /s` directly in the WinRM session (SYSTEM context). Faster, but HKCU keys will not apply to any user.
---
@@ -612,22 +762,21 @@ $kiosks = @("KIOSK-01", "KIOSK-02", "KIOSK-03")
```
**What it does:**
1. Copies installer from network share
2. Runs silent installation
3. Configures auto-start
4. Cleans up installer
1. Pings target PC (skips if offline)
2. Copies installer from network share to `C:\Windows\Temp\`
3. Kills running Edge kiosk
4. Runs silent installation (120-second timeout)
5. Relaunches Edge kiosk via scheduled task as the logged-in user
6. Cleans up installer and scheduled task
**After installation:**
- Run data collection to update PC type
- Reboot PC to complete setup
**No reboot required** — Edge relaunches automatically in the logged-in user's session.
```powershell
# Complete Dashboard deployment sequence
.\Invoke-RemoteMaintenance.ps1 -ComputerName "KIOSK-01" -Task InstallDashboard -Credential $cred
.\Invoke-RemoteMaintenance.ps1 -ComputerName "KIOSK-01" -Task Reboot -Credential $cred
# Deploy to all Dashboard kiosks
.\Invoke-RemoteMaintenance.ps1 -PcType Dashboard -Task InstallDashboard -Credential $cred
# After reboot, update ShopDB
.\Update-ShopfloorPCs-Remote.ps1 -ComputerName "KIOSK-01" -Credential $cred
# Deploy to all Lobby Display kiosks
.\Invoke-RemoteMaintenance.ps1 -PcType "Lobby Display" -Task InstallLobbyDisplay -Credential $cred
```
#### Install Lobby Display

View File

@@ -23,7 +23,7 @@ powershell-scripts/
## Table of Contents
1. [Asset Collection Scripts](#asset-collection-scripts) (`asset-collection/`)
2. [Remote Execution Scripts](#remote-execution-scripts) (`remote-execution/`)
2. [Remote Execution Scripts](#remote-execution-scripts) (`S:\dt\shopfloor\scripts\remote-execution\`)
3. [Setup & Utility Scripts](#setup--utility-scripts) (`setup-utilities/`)
4. [Registry Backup Scripts](#registry-backup-scripts) (`registry-backup/`)
5. [WinRM HTTPS Scripts](#winrm-https-scripts) (`winrm-https/`)

308
docs/convert_to_docx.py Normal file
View File

@@ -0,0 +1,308 @@
#!/usr/bin/env python3
"""
Convert Markdown documentation to Word documents (.docx)
With proper code block formatting (shaded boxes)
"""
import re
import os
from docx import Document
from docx.shared import Inches, Pt, RGBColor, Twips
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.style import WD_STYLE_TYPE
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn, nsmap
from docx.oxml import OxmlElement
def set_cell_shading(cell, color="E8E8E8"):
"""Set cell background shading color."""
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
shd = OxmlElement('w:shd')
shd.set(qn('w:fill'), color)
shd.set(qn('w:val'), 'clear')
tcPr.append(shd)
def set_cell_borders(cell, color="CCCCCC"):
"""Set cell border color."""
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
tcBorders = OxmlElement('w:tcBorders')
for border_name in ['top', 'left', 'bottom', 'right']:
border = OxmlElement(f'w:{border_name}')
border.set(qn('w:val'), 'single')
border.set(qn('w:sz'), '4')
border.set(qn('w:color'), color)
tcBorders.append(border)
tcPr.append(tcBorders)
def add_code_block(doc, code_text, language=""):
"""Add a formatted code block with shading."""
# Create a single-cell table for the code block
table = doc.add_table(rows=1, cols=1)
table.autofit = True
cell = table.rows[0].cells[0]
# Set cell shading (light gray background)
set_cell_shading(cell, "F5F5F5")
set_cell_borders(cell, "DDDDDD")
# Clear default paragraph and add code
cell.paragraphs[0].clear()
# Add each line of code
lines = code_text.split('\n')
for i, line in enumerate(lines):
if i == 0:
para = cell.paragraphs[0]
else:
para = cell.add_paragraph()
para.paragraph_format.space_before = Pt(0)
para.paragraph_format.space_after = Pt(0)
para.paragraph_format.line_spacing = 1.0
run = para.add_run(line if line else ' ') # Use space for empty lines
run.font.name = 'Consolas'
run.font.size = Pt(9)
run.font.color.rgb = RGBColor(0, 0, 0)
# Add spacing after the code block
doc.add_paragraph()
def parse_markdown(md_content):
"""Parse markdown content into structured elements."""
lines = md_content.split('\n')
elements = []
i = 0
while i < len(lines):
line = lines[i]
# Skip empty lines
if not line.strip():
i += 1
continue
# Headers
if line.startswith('# '):
elements.append(('h1', line[2:].strip()))
i += 1
elif line.startswith('## '):
elements.append(('h2', line[3:].strip()))
i += 1
elif line.startswith('### '):
elements.append(('h3', line[4:].strip()))
i += 1
elif line.startswith('#### '):
elements.append(('h4', line[5:].strip()))
i += 1
# Horizontal rule
elif line.strip() == '---':
elements.append(('hr', ''))
i += 1
# Code blocks
elif line.strip().startswith('```'):
code_lang = line.strip()[3:]
code_lines = []
i += 1
while i < len(lines) and not lines[i].strip().startswith('```'):
code_lines.append(lines[i])
i += 1
# Store language info with code
elements.append(('code', (code_lang, '\n'.join(code_lines))))
i += 1 # Skip closing ```
# Tables
elif '|' in line and i + 1 < len(lines) and '---' in lines[i + 1]:
table_lines = [line]
i += 1
while i < len(lines) and '|' in lines[i]:
table_lines.append(lines[i])
i += 1
elements.append(('table', table_lines))
# Bullet lists
elif line.strip().startswith('- ') or line.strip().startswith('* '):
list_items = []
while i < len(lines) and (lines[i].strip().startswith('- ') or lines[i].strip().startswith('* ') or (lines[i].startswith(' ') and lines[i].strip())):
if lines[i].strip().startswith('- ') or lines[i].strip().startswith('* '):
list_items.append(lines[i].strip()[2:])
elif lines[i].startswith(' ') and list_items:
list_items[-1] += ' ' + lines[i].strip()
i += 1
elements.append(('bullet', list_items))
# Numbered lists
elif re.match(r'^\d+\.\s', line.strip()):
list_items = []
while i < len(lines) and (re.match(r'^\d+\.\s', lines[i].strip()) or lines[i].startswith(' ')):
if re.match(r'^\d+\.\s', lines[i].strip()):
list_items.append(re.sub(r'^\d+\.\s', '', lines[i].strip()))
elif lines[i].startswith(' ') and list_items:
list_items[-1] += ' ' + lines[i].strip()
i += 1
elements.append(('numbered', list_items))
# Regular paragraph
else:
para_lines = [line]
i += 1
while i < len(lines) and lines[i].strip() and not lines[i].startswith('#') and not lines[i].startswith('```') and not lines[i].startswith('- ') and not lines[i].startswith('* ') and '|' not in lines[i] and not re.match(r'^\d+\.\s', lines[i].strip()):
para_lines.append(lines[i])
i += 1
elements.append(('para', ' '.join(para_lines)))
return elements
def clean_text(text):
"""Remove markdown formatting from text."""
# Bold
text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text)
# Italic
text = re.sub(r'\*([^*]+)\*', r'\1', text)
# Code
text = re.sub(r'`([^`]+)`', r'\1', text)
# Links
text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text)
return text
def add_formatted_text(paragraph, text):
"""Add text with basic formatting to a paragraph."""
# Split by formatting markers and add runs
parts = re.split(r'(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]+\]\([^)]+\))', text)
for part in parts:
if not part:
continue
if part.startswith('**') and part.endswith('**'):
run = paragraph.add_run(part[2:-2])
run.bold = True
elif part.startswith('`') and part.endswith('`'):
run = paragraph.add_run(part[1:-1])
run.font.name = 'Consolas'
run.font.size = Pt(9)
# Add light background for inline code
run.font.highlight_color = 15 # Light gray (WD_COLOR_INDEX.GRAY_25)
elif part.startswith('[') and '](' in part:
match = re.match(r'\[([^\]]+)\]\(([^)]+)\)', part)
if match:
run = paragraph.add_run(match.group(1))
run.font.color.rgb = RGBColor(0, 0, 255)
run.underline = True
else:
paragraph.add_run(part)
def convert_md_to_docx(md_file, docx_file):
"""Convert a markdown file to a Word document."""
print(f"Converting {md_file} to {docx_file}...")
with open(md_file, 'r', encoding='utf-8') as f:
content = f.read()
elements = parse_markdown(content)
doc = Document()
# Set default font
style = doc.styles['Normal']
style.font.name = 'Calibri'
style.font.size = Pt(11)
for elem_type, elem_content in elements:
if elem_type == 'h1':
p = doc.add_heading(clean_text(elem_content), level=0)
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
elif elem_type == 'h2':
doc.add_heading(clean_text(elem_content), level=1)
elif elem_type == 'h3':
doc.add_heading(clean_text(elem_content), level=2)
elif elem_type == 'h4':
doc.add_heading(clean_text(elem_content), level=3)
elif elem_type == 'hr':
p = doc.add_paragraph()
p.add_run('' * 70)
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
elif elem_type == 'para':
p = doc.add_paragraph()
add_formatted_text(p, elem_content)
elif elem_type == 'code':
code_lang, code_text = elem_content
add_code_block(doc, code_text, code_lang)
elif elem_type == 'bullet':
for item in elem_content:
p = doc.add_paragraph(style='List Bullet')
add_formatted_text(p, item)
elif elem_type == 'numbered':
for item in elem_content:
p = doc.add_paragraph(style='List Number')
add_formatted_text(p, item)
elif elem_type == 'table':
# Parse table
rows = []
for line in elem_content:
if '---' in line:
continue
cells = [c.strip() for c in line.split('|')[1:-1]]
if cells:
rows.append(cells)
if rows:
num_cols = len(rows[0])
table = doc.add_table(rows=len(rows), cols=num_cols)
table.style = 'Table Grid'
table.alignment = WD_TABLE_ALIGNMENT.CENTER
for i, row in enumerate(rows):
for j, cell in enumerate(row):
if j < num_cols:
table.rows[i].cells[j].text = clean_text(cell)
# Bold and shade header row
if i == 0:
set_cell_shading(table.rows[i].cells[j], "E0E0E0")
for para in table.rows[i].cells[j].paragraphs:
for run in para.runs:
run.bold = True
# Add spacing after table
doc.add_paragraph()
doc.save(docx_file)
print(f" Created: {docx_file}")
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'
]
for md_file in md_files:
md_path = os.path.join(docs_dir, md_file)
docx_path = os.path.join(docs_dir, md_file.replace('.md', '.docx'))
if os.path.exists(md_path):
convert_md_to_docx(md_path, docx_path)
else:
print(f"Warning: {md_path} not found")
print("\nConversion complete!")
print(f"Word documents saved to: {docs_dir}")
if __name__ == '__main__':
main()