<# .SYNOPSIS Appends a dated entry to the Daniel/Ahmed 1:1 notebook tracker (.docx) using Word COM. .DESCRIPTION Clones the formatting of the existing "Example:" block (numbered question headings at list level 1, lettered content bullets at list level 2) so new entries match the tracker's look exactly, without touching the document's MIP sensitivity label or any existing content. Every entry starts on a new page: a manual page break (Ctrl+Enter) is emitted ahead of the date heading. Entry content is supplied as a JSON file so the caller never has to fight PowerShell quoting. Shape: { "achievements": ["bullet one", "bullet two"], "next": ["bullet"], "lessons": ["bullet"], "topics": ["bullet"] } Any section may be omitted or empty; it renders a single "None." bullet. .PARAMETER DocPath Full path to the tracker .docx. .PARAMETER JsonPath Full path to the UTF-8 JSON file described above. .PARAMETER EntryDate Date heading for the entry, MM/DD/yyyy. Defaults to today. .PARAMETER Position Top - insert as the newest entry, immediately after the Example block (default). Bottom - append at the end of the document (chronological). .PARAMETER Preview Render the entry to the console and exit without modifying the document. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$DocPath, [Parameter(Mandatory = $true)][string]$JsonPath, [string]$EntryDate = (Get-Date -Format 'MM/dd/yyyy'), [ValidateSet('Top', 'Bottom')][string]$Position = 'Top', [switch]$Preview ) $ErrorActionPreference = 'Stop' $SectionHeadings = @( 'What are my Achievements, Impact and Key Results?', "What's next? / What am I working on?", 'Lessons Learned/Things to Improve', 'Topics to discuss/What I need help with' ) if (-not (Test-Path -LiteralPath $DocPath)) { throw "Tracker not found: $DocPath" } if (-not (Test-Path -LiteralPath $JsonPath)) { throw "Entry JSON not found: $JsonPath" } $entry = Get-Content -LiteralPath $JsonPath -Raw -Encoding UTF8 | ConvertFrom-Json function Get-Bullets($value) { if ($null -eq $value) { return , @('None.') } $list = @($value | Where-Object { $_ -and $_.ToString().Trim() } | ForEach-Object { $_.ToString().Trim() }) if ($list.Count -eq 0) { return , @('None.') } return , $list } $sections = @( (Get-Bullets $entry.achievements), (Get-Bullets $entry.next), (Get-Bullets $entry.lessons), (Get-Bullets $entry.topics) ) if ($Preview) { Write-Output $EntryDate for ($i = 0; $i -lt 4; $i++) { Write-Output (" {0}. {1}" -f ($i + 1), $SectionHeadings[$i]) foreach ($b in $sections[$i]) { Write-Output " - $b" } } return } # --- Word COM ------------------------------------------------------------- $wdCharacter = 1 $word = $null $doc = $null $openedByUs = $false function Get-ParaText($para) { return ($para.Range.Text -replace "[\r\a]", '') } function Set-ParaText($para, [string]$text) { $r = $para.Range if ($r.End - $r.Start -gt 0) { [void]$r.MoveEnd($wdCharacter, -1) } $r.Text = $text } try { try { $word = [Runtime.InteropServices.Marshal]::GetActiveObject('Word.Application') } catch { $word = New-Object -ComObject Word.Application; $word.Visible = $false } $full = (Resolve-Path -LiteralPath $DocPath).Path foreach ($d in $word.Documents) { if ($d.FullName -eq $full) { $doc = $d; break } } if (-not $doc) { $doc = $word.Documents.Open($full); $openedByUs = $true } # Locate the Example block: the first bare MM/DD/YYYY paragraph through the paragraph # before the next bare-date paragraph (or the last non-empty paragraph). $paras = $doc.Paragraphs $dateIdx = @() for ($i = 1; $i -le $paras.Count; $i++) { if ((Get-ParaText $paras.Item($i)).Trim() -match '^\d{1,2}/\d{1,2}/\d{4}$') { $dateIdx += $i } } if ($dateIdx.Count -eq 0) { throw 'Could not find the Example block (no MM/DD/YYYY paragraph) in the tracker.' } $exStart = $dateIdx[0] $exEnd = if ($dateIdx.Count -gt 1) { $dateIdx[1] - 1 } else { $paras.Count } while ($exEnd -gt $exStart -and (Get-ParaText $paras.Item($exEnd)).Trim() -eq '') { $exEnd-- } # Formatting seeds harvested from the Example block. $seedPlain = $null # bare paragraph (the date line) $seedHead = $null # list level 1 (the four questions) $seedBullet = $null # list level 2 (content under a question) for ($i = $exStart; $i -le $exEnd; $i++) { $p = $paras.Item($i) $lvl = $p.Range.ListFormat.ListLevelNumber if ($i -eq $exStart) { $seedPlain = $p.Range } elseif ($lvl -eq 1 -and -not $seedHead) { $seedHead = $p.Range } elseif ($lvl -eq 2 -and -not $seedBullet) { $seedBullet = $p.Range } } if (-not $seedPlain -or -not $seedHead -or -not $seedBullet) { throw 'Example block is missing a plain / level-1 / level-2 paragraph to use as a formatting seed.' } # Snapshot the seeds' formatted text before inserting (live ranges shift as we edit). $fmtPlain = $seedPlain.FormattedText $fmtHead = $seedHead.FormattedText $fmtBullet = $seedBullet.FormattedText # Insertion point: end of the Example block (Top) or end of the document (Bottom). if ($Position -eq 'Top') { $pos = $paras.Item($exEnd).Range.End } else { $lastIdx = $paras.Count while ($lastIdx -gt 1 -and (Get-ParaText $paras.Item($lastIdx)).Trim() -eq '') { $lastIdx-- } $pos = $paras.Item($lastIdx).Range.End } # Emit one paragraph cloned from a seed, set its text, advance the cursor. $emit = { param($fmt, [string]$text) $r = $doc.Range($pos, $pos) $r.FormattedText = $fmt $p = $doc.Range($r.Start, $r.End).Paragraphs.Item(1) Set-ParaText $p $text $script:pos = $p.Range.End } # Each entry starts on its own page: chr(12) is Word's manual page break (Ctrl+Enter), # embedded at the head of the date line so no extra paragraph is introduced. & $emit $fmtPlain ("$([char]12)$EntryDate") for ($s = 0; $s -lt 4; $s++) { & $emit $fmtHead $SectionHeadings[$s] foreach ($b in $sections[$s]) { & $emit $fmtBullet $b } } # Strip stray page-break characters Word may leave in trailing empty paragraphs, # which would otherwise render a blank page at the end of the document. for ($i = $doc.Paragraphs.Count; $i -ge 1; $i--) { $t = $doc.Paragraphs.Item($i).Range.Text -replace "[\r\a]", '' if ($t.Trim() -ne '') { break } if ($t -match [string][char]12) { Set-ParaText $doc.Paragraphs.Item($i) '' } } $doc.Save() Write-Output "Wrote entry $EntryDate to $full (position: $Position)." } finally { if ($doc -and $openedByUs) { $doc.Close([ref]$true) | Out-Null } if ($word -and $openedByUs -and $word.Documents.Count -eq 0) { $word.Quit() | Out-Null } [System.GC]::Collect() }