Import current resume static site
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$siteRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).ProviderPath
|
||||
$python = "C:\Users\jason\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe"
|
||||
$chrome = "C:\Program Files\Google\Chrome\Application\chrome.exe"
|
||||
$port = 8793
|
||||
$pdfPath = Join-Path $siteRoot "Resume_Jason_Eugene_Garrison.pdf"
|
||||
$docxPath = Join-Path $siteRoot "Resume_Jason_Eugene_Garrison.docx"
|
||||
|
||||
if (-not (Test-Path $python)) {
|
||||
throw "Bundled Python was not found at $python"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $chrome)) {
|
||||
throw "Chrome was not found at $chrome"
|
||||
}
|
||||
|
||||
& $python (Join-Path $PSScriptRoot "generate-resume-docx.py") $siteRoot $docxPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "DOCX generation failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
$server = $null
|
||||
try {
|
||||
$server = Start-Process -FilePath $python `
|
||||
-ArgumentList @("-m", "http.server", "$port", "--bind", "127.0.0.1", "--directory", $siteRoot) `
|
||||
-WorkingDirectory $env:TEMP `
|
||||
-WindowStyle Hidden `
|
||||
-PassThru
|
||||
|
||||
$url = "http://127.0.0.1:$port/index.html?print=$(Get-Date -Format yyyyMMddHHmmss)"
|
||||
|
||||
$ready = $false
|
||||
for ($i = 0; $i -lt 30; $i++) {
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 2
|
||||
if ($response.StatusCode -eq 200 -and $response.Content -match "Jason Eugene Garrison") {
|
||||
$ready = $true
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $ready) {
|
||||
throw "Local resume preview server was not reachable at $url"
|
||||
}
|
||||
|
||||
& $chrome `
|
||||
--headless=new `
|
||||
--disable-gpu `
|
||||
--disable-background-networking `
|
||||
--disable-sync `
|
||||
--metrics-recording-only `
|
||||
--no-pdf-header-footer `
|
||||
--print-to-pdf="$pdfPath" `
|
||||
$url | Out-Null
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "PDF generation failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($server -and -not $server.HasExited) {
|
||||
Stop-Process -Id $server.Id -Force
|
||||
}
|
||||
}
|
||||
|
||||
& (Join-Path $PSScriptRoot "publish-permissions.ps1")
|
||||
|
||||
Write-Host "Updated $pdfPath"
|
||||
Write-Host "Updated $docxPath"
|
||||
@@ -0,0 +1,214 @@
|
||||
from pathlib import Path
|
||||
import html
|
||||
import re
|
||||
import sys
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.section import WD_SECTION
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Inches, Pt, RGBColor
|
||||
from lxml import html as lxml_html
|
||||
|
||||
|
||||
CONTACTS = {
|
||||
"Location": "Oak Grove, KY",
|
||||
"Phone (KY)": "(270) 889-8072",
|
||||
"Phone (TN)": "(865) 985-1641",
|
||||
"Email": "jasoneugenegarrison@icloud.com",
|
||||
"Website": "www.jasoneugenegarrison.com",
|
||||
}
|
||||
|
||||
|
||||
def clean(text):
|
||||
return re.sub(r"\s+", " ", html.unescape(text or "")).strip()
|
||||
|
||||
|
||||
def by_class(node, class_name, tag="*"):
|
||||
xpath = f".//{tag}[contains(concat(' ', normalize-space(@class), ' '), ' {class_name} ')]"
|
||||
return node.xpath(xpath)
|
||||
|
||||
|
||||
def add_bottom_border(paragraph, color="b91c1c", size="12"):
|
||||
p_pr = paragraph._p.get_or_add_pPr()
|
||||
p_bdr = OxmlElement("w:pBdr")
|
||||
bottom = OxmlElement("w:bottom")
|
||||
bottom.set(qn("w:val"), "single")
|
||||
bottom.set(qn("w:sz"), size)
|
||||
bottom.set(qn("w:space"), "4")
|
||||
bottom.set(qn("w:color"), color)
|
||||
p_bdr.append(bottom)
|
||||
p_pr.append(p_bdr)
|
||||
|
||||
|
||||
def set_run(run, size=None, bold=None, color=None, all_caps=False):
|
||||
if size:
|
||||
run.font.size = Pt(size)
|
||||
if bold is not None:
|
||||
run.bold = bold
|
||||
if color:
|
||||
run.font.color.rgb = RGBColor.from_string(color)
|
||||
if all_caps:
|
||||
run.text = run.text.upper()
|
||||
|
||||
|
||||
def add_heading(doc, text):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_before = Pt(12)
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
run = p.add_run(text.upper())
|
||||
run.bold = True
|
||||
run.font.name = "Arial"
|
||||
run.font.size = Pt(11)
|
||||
run.font.color.rgb = RGBColor(185, 28, 28)
|
||||
add_bottom_border(p, "e5e7eb", "6")
|
||||
return p
|
||||
|
||||
|
||||
def add_bullet(doc, text):
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.left_indent = Inches(0.22)
|
||||
p.paragraph_format.first_line_indent = Inches(-0.12)
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
run = p.add_run(text)
|
||||
run.font.name = "Arial"
|
||||
run.font.size = Pt(9.5)
|
||||
return p
|
||||
|
||||
|
||||
def build_docx(site_root, output_path):
|
||||
index_path = site_root / "index.html"
|
||||
tree = lxml_html.fromstring(index_path.read_text(encoding="utf-8"))
|
||||
|
||||
name = clean(tree.xpath("string(//h1)"))
|
||||
role = clean(tree.xpath("string(//*[contains(@class, 'role')])"))
|
||||
availability = clean(tree.xpath("string(//*[contains(@class, 'availability')])"))
|
||||
strengths = [clean(el.text_content()) for el in tree.xpath(".//*[contains(concat(' ', normalize-space(@class), ' '), ' strength-cloud ')]//span")]
|
||||
|
||||
sections = {}
|
||||
for section in tree.xpath(".//section[contains(concat(' ', normalize-space(@class), ' '), ' section ')]"):
|
||||
title = clean(section.xpath("string(.//*[contains(@class, 'section-title')])"))
|
||||
sections[title] = section
|
||||
|
||||
doc = Document()
|
||||
section = doc.sections[0]
|
||||
section.top_margin = Inches(0.55)
|
||||
section.bottom_margin = Inches(0.55)
|
||||
section.left_margin = Inches(0.7)
|
||||
section.right_margin = Inches(0.7)
|
||||
|
||||
styles = doc.styles
|
||||
styles["Normal"].font.name = "Arial"
|
||||
styles["Normal"].font.size = Pt(9.5)
|
||||
|
||||
title = doc.add_paragraph()
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
title.paragraph_format.space_after = Pt(2)
|
||||
r = title.add_run(name.upper())
|
||||
set_run(r, size=18, bold=True, color="111827")
|
||||
|
||||
subtitle = doc.add_paragraph()
|
||||
subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
subtitle.paragraph_format.space_after = Pt(4)
|
||||
r = subtitle.add_run(role)
|
||||
set_run(r, size=10.5, bold=True, color="b91c1c")
|
||||
|
||||
avail = doc.add_paragraph()
|
||||
avail.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
avail.paragraph_format.space_after = Pt(6)
|
||||
r = avail.add_run(availability)
|
||||
set_run(r, size=8.8, color="4b5563")
|
||||
|
||||
contact_line = " | ".join(f"{label}: {value}" for label, value in CONTACTS.items())
|
||||
contact = doc.add_paragraph()
|
||||
contact.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
contact.paragraph_format.space_after = Pt(8)
|
||||
r = contact.add_run(contact_line)
|
||||
set_run(r, size=7.8, color="374151")
|
||||
add_bottom_border(contact, "b91c1c", "10")
|
||||
|
||||
profile_section = sections.get("Professional Profile")
|
||||
if profile_section is not None:
|
||||
add_heading(doc, "Professional Profile")
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(5)
|
||||
r = p.add_run(clean(profile_section.xpath("string(.//*[contains(@class, 'profile-text')])")))
|
||||
set_run(r, size=9.5, color="111827")
|
||||
|
||||
if strengths:
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
r = p.add_run("Core strengths: ")
|
||||
set_run(r, size=9, bold=True, color="111827")
|
||||
r = p.add_run(", ".join(strengths))
|
||||
set_run(r, size=9, color="374151")
|
||||
|
||||
tools_section = sections.get("Core Tools & Work Types")
|
||||
if tools_section is not None:
|
||||
add_heading(doc, "Core Tools & Work Types")
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
r = p.add_run(clean(tools_section.xpath("string(.//*[contains(@class, 'profile-text')])")))
|
||||
set_run(r, size=9.2, color="111827")
|
||||
|
||||
experience_section = sections.get("Professional Experience")
|
||||
if experience_section is not None:
|
||||
add_heading(doc, "Professional Experience")
|
||||
for job in by_class(experience_section, "job-item"):
|
||||
head = doc.add_paragraph()
|
||||
head.paragraph_format.space_before = Pt(4)
|
||||
head.paragraph_format.space_after = Pt(1)
|
||||
title_text = clean(job.xpath("string(.//*[contains(@class, 'job-title')])"))
|
||||
date_text = clean(job.xpath("string(.//*[contains(@class, 'job-meta')])"))
|
||||
r = head.add_run(title_text)
|
||||
set_run(r, size=10.2, bold=True, color="111827")
|
||||
r = head.add_run(f" | {date_text}")
|
||||
set_run(r, size=9, bold=True, color="b91c1c")
|
||||
|
||||
sub = doc.add_paragraph()
|
||||
sub.paragraph_format.space_after = Pt(2)
|
||||
r = sub.add_run(clean(job.xpath("string(.//*[contains(@class, 'job-sub')])")))
|
||||
set_run(r, size=8.8, bold=True, color="6b7280")
|
||||
|
||||
for li in by_class(job, "job-desc"):
|
||||
for item in li.xpath(".//li"):
|
||||
add_bullet(doc, clean(item.text_content()))
|
||||
|
||||
education_section = sections.get("Education & Background")
|
||||
if education_section is not None:
|
||||
add_heading(doc, "Education & Background")
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(1)
|
||||
r = p.add_run(clean(education_section.xpath("string(.//*[contains(@class, 'edu-title')])")))
|
||||
set_run(r, size=9.8, bold=True, color="111827")
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
r = p.add_run(clean(education_section.xpath("string(.//*[contains(@class, 'edu-sub')])")))
|
||||
set_run(r, size=8.8, color="6b7280")
|
||||
for paragraph in by_class(education_section, "edu-text"):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
r = p.add_run(clean(paragraph.text_content()))
|
||||
set_run(r, size=9.2, color="111827")
|
||||
|
||||
target_section = sections.get("Career Target")
|
||||
if target_section is not None:
|
||||
add_heading(doc, "Career Target")
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
r = p.add_run(clean(target_section.xpath("string(.//*[contains(@class, 'profile-text')])")))
|
||||
set_run(r, size=9.5, color="111827")
|
||||
|
||||
footer = section.footer.paragraphs[0]
|
||||
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
r = footer.add_run("Jason Eugene Garrison | Resume | Last updated June 15, 2026")
|
||||
set_run(r, size=7.5, color="6b7280")
|
||||
|
||||
doc.save(output_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = Path(sys.argv[1]).resolve()
|
||||
out = Path(sys.argv[2]).resolve()
|
||||
build_docx(root, out)
|
||||
@@ -0,0 +1,39 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$siteRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).ProviderPath
|
||||
|
||||
$publicFiles = @(
|
||||
"index.html",
|
||||
"site.css",
|
||||
"site.js",
|
||||
"Resume_Jason_Eugene_Garrison.pdf",
|
||||
"Resume_Jason_Eugene_Garrison.docx"
|
||||
)
|
||||
|
||||
foreach ($file in $publicFiles) {
|
||||
$path = Join-Path $siteRoot $file
|
||||
if (Test-Path -LiteralPath $path) {
|
||||
try {
|
||||
icacls $path /grant "*S-1-1-0:RX" | Out-Null
|
||||
} catch {
|
||||
Write-Warning "Could not update public permissions for ${file}: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($privatePath in @(".old", "scripts", "README.md", ".htaccess")) {
|
||||
$path = Join-Path $siteRoot $privatePath
|
||||
if (Test-Path -LiteralPath $path) {
|
||||
try {
|
||||
if ($privatePath -eq ".old") {
|
||||
icacls $path /remove:g "*S-1-1-0" | Out-Null
|
||||
} else {
|
||||
icacls $path /remove:g "*S-1-1-0" /T | Out-Null
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Could not update private permissions for ${privatePath}: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Published file permissions updated."
|
||||
@@ -0,0 +1,33 @@
|
||||
param(
|
||||
[string]$Label = "snapshot"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$siteRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).ProviderPath
|
||||
$oldRoot = Join-Path $siteRoot ".old"
|
||||
$safeLabel = ($Label -replace "[^A-Za-z0-9._-]", "-").Trim("-")
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($safeLabel)) {
|
||||
$safeLabel = "snapshot"
|
||||
}
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$snapshot = Join-Path $oldRoot "$timestamp-$safeLabel"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $snapshot | Out-Null
|
||||
|
||||
$oldDir = Get-Item -LiteralPath $oldRoot -Force
|
||||
$oldDir.Attributes = $oldDir.Attributes -bor [System.IO.FileAttributes]::Hidden
|
||||
|
||||
$excludeDirs = @(".git", ".old")
|
||||
|
||||
Get-ChildItem -LiteralPath $siteRoot -Force | Where-Object {
|
||||
$excludeDirs -notcontains $_.Name
|
||||
} | ForEach-Object {
|
||||
Copy-Item -LiteralPath $_.FullName -Destination $snapshot -Recurse -Force
|
||||
}
|
||||
|
||||
icacls (Resolve-Path -LiteralPath $snapshot).ProviderPath /remove:g "*S-1-1-0" /T | Out-Null
|
||||
|
||||
Write-Host "Saved snapshot: $snapshot"
|
||||
Reference in New Issue
Block a user