# ==============================================================================
# FolderHierarchy.ps1
# Markdown / Text -> Folder & File Hierarchy Creator
#
# FLOW
# Step 1 Input file
# Step 2 Pilih range baris
# Step 3 Normalisasi indentasi
# Step 4 Mode pembuatan (Folder Hierarchy / File Batch)
# Step 5 Tipe output <- TABEL PILIHAN (Folder/.md/.txt/.rst/.yaml/.yml)
# Step 6 Direktori target
# Step 7 Preview + konfirmasi sebelum export
# Step 8 Opsi isi konten file
# Step 9 Eksekusi
# ==============================================================================
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ------------------------------------------------------------------------------
# HELPERS
# ------------------------------------------------------------------------------
function Write-Banner {
param([string]$Text)
Write-Host ""
Write-Host ("=" * 64) -ForegroundColor DarkCyan
Write-Host " $Text" -ForegroundColor Cyan
Write-Host ("=" * 64) -ForegroundColor DarkCyan
}
function Write-Section {
param([string]$No, [string]$Title)
Write-Host ""
Write-Host ("-" * 64) -ForegroundColor DarkGray
Write-Host (" STEP $No | $Title") -ForegroundColor Yellow
Write-Host ("-" * 64) -ForegroundColor DarkGray
}
function Write-OK { param([string]$msg) Write-Host " [OK] $msg" -ForegroundColor Green }
function Write-WARN { param([string]$msg) Write-Host " [!!] $msg" -ForegroundColor Yellow }
function Write-ERR { param([string]$msg) Write-Host " [XX] $msg" -ForegroundColor Red }
function Write-SKIP { param([string]$msg) Write-Host " [--] $msg" -ForegroundColor DarkGray }
function Read-YN {
param([string]$Prompt = 'Lanjutkan? (Y/N)')
return ((Read-Host "`n $Prompt").Trim().ToLower() -eq 'y')
}
# ------------------------------------------------------------------------------
# TABEL TIPE OUTPUT
# Kolom: No | Nama File | File Extension
# ------------------------------------------------------------------------------
$TYPE_TABLE = @(
[PSCustomObject]@{ No=1; Label='📁 Folder'; Ext='(folder)'; IsFolder=$true }
[PSCustomObject]@{ No=2; Label='Markdown'; Ext='.md'; IsFolder=$false }
[PSCustomObject]@{ No=3; Label='Text'; Ext='.txt'; IsFolder=$false }
[PSCustomObject]@{ No=4; Label='ReStructuredText'; Ext='.rst'; IsFolder=$false }
[PSCustomObject]@{ No=5; Label='YAML'; Ext='.yaml'; IsFolder=$false }
[PSCustomObject]@{ No=6; Label='YAML'; Ext='.yml'; IsFolder=$false }
)
function Show-TypeTable {
Write-Host ""
Write-Host (" {0,-5} {1,-24} {2}" -f 'No', 'Nama File', 'File Extension') `
-ForegroundColor Cyan
Write-Host (" {0,-5} {1,-24} {2}" -f '-----', '------------------------', '--------------') `
-ForegroundColor DarkGray
foreach ($row in $TYPE_TABLE) {
$noStr = " {0,-5} " -f $row.No
$nmStr = "{0,-24} " -f $row.Label
$extStr = "{0}" -f $row.Ext
if ($row.IsFolder) {
Write-Host -NoNewline $noStr -ForegroundColor White
Write-Host -NoNewline $nmStr -ForegroundColor DarkYellow
Write-Host $extStr -ForegroundColor DarkYellow
} else {
Write-Host -NoNewline $noStr -ForegroundColor White
Write-Host -NoNewline $nmStr -ForegroundColor White
Write-Host $extStr -ForegroundColor Green
}
}
Write-Host ""
}
function Read-TypeChoice {
Show-TypeTable
do {
$raw = (Read-Host " Pilih No (1-$($TYPE_TABLE.Count))").Trim()
$num = 0
$valid = [int]::TryParse($raw, [ref]$num) -and $num -ge 1 -and $num -le $TYPE_TABLE.Count
if (-not $valid) { Write-ERR "Masukkan angka 1 sampai $($TYPE_TABLE.Count)." }
} until ($valid)
return $TYPE_TABLE[$num - 1]
}
# ------------------------------------------------------------------------------
# STEP 1 Input file
# ------------------------------------------------------------------------------
Write-Banner 'FolderHierarchy.ps1 | Hierarchy Creator'
Write-Section '1' 'Input File'
do {
$inputFile = (Read-Host "`n Path ke file (.md / .txt)").Trim()
if (-not (Test-Path $inputFile)) {
Write-ERR 'File tidak ditemukan, coba lagi.'
$inputOk = $false
} else { $inputOk = $true }
} until ($inputOk)
[string[]]$rawContent = Get-Content $inputFile -Encoding UTF8
if ($rawContent -is [string]) { $rawContent = @($rawContent) }
Write-OK "Dimuat: $inputFile ($($rawContent.Count) baris)"
# ------------------------------------------------------------------------------
# STEP 2 Pilih range baris
# ------------------------------------------------------------------------------
Write-Section '2' 'Pilih Range Baris'
Write-Host ""
for ($i = 0; $i -lt $rawContent.Count; $i++) {
Write-Host (" {0,4} | {1}" -f ($i + 1), $rawContent[$i])
}
$rangeOk = $false
while (-not $rangeOk) {
do {
$rawRange = Read-Host "`n Range baris (contoh: 1-20 | Enter = semua)"
if ([string]::IsNullOrWhiteSpace($rawRange)) {
$s = 1; $e = $rawContent.Count; $parseOk = $true
} elseif ($rawRange -match '^\s*(\d+)\s*-\s*(\d+)\s*$') {
$s = [int]$Matches[1]; $e = [int]$Matches[2]
if ($s -ge 1 -and $e -le $rawContent.Count -and $s -le $e) {
$parseOk = $true
} else {
Write-ERR "Di luar jangkauan (1-$($rawContent.Count))."
$parseOk = $false
}
} else {
Write-ERR "Format salah. Contoh: 3-15"
$parseOk = $false
}
} until ($parseOk)
[string[]]$selectedLines = $rawContent[($s-1)..($e-1)]
Write-Host ""
Write-Host " Baris terpilih:" -ForegroundColor Cyan
for ($i = 0; $i -lt $selectedLines.Count; $i++) {
Write-Host (" {0,4} | {1}" -f ($s + $i), $selectedLines[$i])
}
$rangeOk = Read-YN 'Gunakan pilihan ini? (Y/N)'
if (-not $rangeOk) { Write-WARN 'Kembali ke pemilihan baris...' }
}
# ------------------------------------------------------------------------------
# STEP 3 Normalisasi indentasi
# ------------------------------------------------------------------------------
Write-Section '3' 'Normalisasi Indentasi'
function Get-TabCount ([string]$line) {
return $line.Length - $line.TrimStart("`t").Length
}
$baseTab = Get-TabCount $selectedLines[0]
$normList = [System.Collections.Generic.List[string]]::new()
foreach ($ln in $selectedLines) {
if ([string]::IsNullOrWhiteSpace($ln)) { continue }
if ((Get-TabCount $ln) -ge $baseTab) { $normList.Add($ln.Substring($baseTab)) }
}
Write-Host ""
Write-Host " Normalized tree:" -ForegroundColor Cyan
foreach ($ln in $normList) { Write-Host " | $ln" }
# ------------------------------------------------------------------------------
# STEP 4 Mode pembuatan
# ------------------------------------------------------------------------------
Write-Section '4' 'Mode Pembuatan'
Write-Host ""
Write-Host " [1] Folder Hierarchy - buat tree direktori dari indentasi" -ForegroundColor White
Write-Host " [2] File Batch - buat file dari list (satu path per baris)" -ForegroundColor White
Write-Host ""
do {
$modeRaw = (Read-Host " Mode (1 atau 2)").Trim()
} until ($modeRaw -eq '1' -or $modeRaw -eq '2')
$createMode = [int]$modeRaw
Write-OK "Mode: [$createMode] $(if ($createMode -eq 1) {'Folder Hierarchy'} else {'File Batch'})"
# ------------------------------------------------------------------------------
# STEP 5 Tipe output <-- TABEL PILIHAN
# ------------------------------------------------------------------------------
Write-Section '5' 'Pilih Tipe Output'
$chosen = Read-TypeChoice
Write-OK ("Dipilih: [{0}] {1} -> {2}" -f $chosen.No, $chosen.Label, $chosen.Ext)
if ($createMode -eq 1 -and -not $chosen.IsFolder) {
Write-Host ""
Write-WARN "Mode Folder Hierarchy + tipe file dipilih."
Write-WARN "Setiap node tree dibuat sebagai FILE dengan ekstensi '$($chosen.Ext)'."
}
if ($createMode -eq 2 -and $chosen.IsFolder) {
Write-Host ""
Write-WARN "Mode File Batch + tipe Folder dipilih."
Write-WARN "Setiap baris akan dibuat sebagai FOLDER."
}
# ------------------------------------------------------------------------------
# STEP 6 Direktori target
# ------------------------------------------------------------------------------
Write-Section '6' 'Direktori Target'
do {
$targetDir = (Read-Host "`n Parent folder tujuan").Trim()
if (-not (Test-Path $targetDir)) {
if (Read-YN 'Folder belum ada. Buat sekarang? (Y/N)') {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
Write-OK "Folder dibuat: $targetDir"
$dirOk = $true
} else { $dirOk = $false }
} else {
Write-OK "Folder ditemukan: $targetDir"
$dirOk = $true
}
} until ($dirOk)
# ------------------------------------------------------------------------------
# STEP 7 Bangun preview list + konfirmasi
# ------------------------------------------------------------------------------
Write-Section '7' 'Preview Item yang Akan Dibuat'
$previewList = [System.Collections.Generic.List[string]]::new()
if ($createMode -eq 1) {
$stack = [System.Collections.Generic.List[string]]::new()
foreach ($ln in $normList) {
if ([string]::IsNullOrWhiteSpace($ln)) { continue }
$trimmed = $ln.TrimStart("`t")
$depth = $ln.Length - $trimmed.Length
while ($stack.Count -gt $depth) { $stack.RemoveAt($stack.Count - 1) }
$stack.Add($trimmed)
$rel = $stack -join [System.IO.Path]::DirectorySeparatorChar
$path = if ($chosen.IsFolder) {
Join-Path $targetDir $rel
} else {
Join-Path $targetDir ($rel + $chosen.Ext)
}
$previewList.Add($path)
}
} else {
foreach ($ln in $normList) {
if ([string]::IsNullOrWhiteSpace($ln)) { continue }
$name = $ln.Trim()
$path = if ($chosen.IsFolder) {
Join-Path $targetDir $name
} elseif ($name.EndsWith($chosen.Ext)) {
Join-Path $targetDir $name
} else {
Join-Path $targetDir ($name + $chosen.Ext)
}
$previewList.Add($path)
}
}
Write-Host ""
Write-Host (" {0,-5} {1,-8} {2,-6} {3}" -f 'No', 'Action', 'Tipe', 'Path') -ForegroundColor Cyan
Write-Host (" {0,-5} {1,-8} {2,-6} {3}" -f '-----', '--------', '------', '--------------------------------------------') -ForegroundColor DarkGray
$idx = 1
foreach ($p in $previewList) {
$exists = Test-Path $p
$action = if ($exists) { 'SKIP' } else { 'CREATE' }
$tipeStr = if ($chosen.IsFolder) { 'DIR' } else { 'FILE' }
$rowCol = if ($exists) { 'DarkGray' } else { 'White' }
$actCol = if ($exists) { 'DarkGray' } else { 'Green' }
Write-Host -NoNewline (" {0,-5} " -f $idx) -ForegroundColor $rowCol
Write-Host -NoNewline ("{0,-8} " -f $action) -ForegroundColor $actCol
Write-Host -NoNewline ("{0,-6} " -f $tipeStr) -ForegroundColor DarkYellow
Write-Host $p -ForegroundColor $rowCol
$idx++
}
$willCreate = ($previewList | Where-Object { -not (Test-Path $_) }).Count
$willSkip = $previewList.Count - $willCreate
Write-Host ""
Write-Host (" Ringkasan: {0} akan dibuat | {1} dilewati (sudah ada)" -f $willCreate, $willSkip) -ForegroundColor Cyan
if (-not (Read-YN 'Lanjutkan pembuatan? (Y/N)')) {
Write-WARN 'Dibatalkan. Tidak ada yang dibuat.'
exit 0
}
# ------------------------------------------------------------------------------
# STEP 8 Opsi isi konten file
# ------------------------------------------------------------------------------
$contentMode = '1'
$customContent = ''
if (-not $chosen.IsFolder) {
Write-Section '8' 'Opsi Isi Konten File'
Write-Host ""
Write-Host " [1] File kosong" -ForegroundColor White
Write-Host " [2] Header komentar otomatis" -ForegroundColor White
Write-Host " [3] Konten custom (sama untuk semua file)" -ForegroundColor White
Write-Host ""
do {
$contentMode = (Read-Host " Pilih opsi (1/2/3)").Trim()
} until ($contentMode -in @('1','2','3'))
if ($contentMode -eq '3') {
Write-Host " Masukkan konten (gunakan \n untuk baris baru):" -ForegroundColor Yellow
$customContent = (Read-Host " Konten").Replace('\n', "`n")
}
} else {
Write-Section '8' 'Opsi Isi Konten File'
Write-Host " Tipe Folder dipilih - step ini dilewati." -ForegroundColor DarkGray
}
# ------------------------------------------------------------------------------
# STEP 9 Eksekusi
# ------------------------------------------------------------------------------
Write-Section '9' 'Membuat Item'
Write-Host ""
$totalCreated = 0
$totalSkipped = 0
foreach ($fullPath in $previewList) {
$isDir = $chosen.IsFolder
$parentD = if ($isDir) { $fullPath } else { Split-Path $fullPath -Parent }
if (-not (Test-Path $parentD)) {
New-Item -ItemType Directory -Path $parentD -Force | Out-Null
}
if ($isDir) {
if (Test-Path $fullPath) {
Write-SKIP $fullPath; $totalSkipped++
} else {
New-Item -ItemType Directory -Path $fullPath -Force | Out-Null
Write-OK $fullPath; $totalCreated++
}
} else {
if (Test-Path $fullPath) {
Write-SKIP $fullPath; $totalSkipped++
} else {
switch ($contentMode) {
'1' {
[System.IO.File]::WriteAllText($fullPath, '', [System.Text.Encoding]::UTF8)
}
'2' {
$hdr = "# $(Split-Path $fullPath -Leaf)`n" +
"# Auto-generated by FolderHierarchy.ps1`n" +
"# $(Get-Date -Format 'yyyy-MM-dd HH:mm')`n"
[System.IO.File]::WriteAllText($fullPath, $hdr, [System.Text.Encoding]::UTF8)
}
'3' {
[System.IO.File]::WriteAllText($fullPath, $customContent, [System.Text.Encoding]::UTF8)
}
}
Write-OK $fullPath; $totalCreated++
}
}
}
# ------------------------------------------------------------------------------
# SELESAI
# ------------------------------------------------------------------------------
Write-Host ""
Write-Host ("=" * 64) -ForegroundColor DarkCyan
Write-Host (" SELESAI | Dibuat: $totalCreated Dilewati: $totalSkipped") -ForegroundColor Green
Write-Host ("=" * 64) -ForegroundColor DarkCyan
Write-Host ""