NoNama FileFile Extension
1📁Folder(folder)
2Markdown.md
3Text.txt
4ReStructuredText.rst
5YAML.yaml
6YAML.yml
# ==============================================================================
#  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 ""