Guest User

merger.ps1

a guest
Oct 31st, 2025
30
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PowerShell 7.07 KB | Source Code | 0 0
  1. <#
  2. .SYNOPSIS
  3.     Merges two Netscape bookmark HTML files.
  4.     - Merges same-name folders (case-insensitive)
  5.     - Removes duplicates BY URL or BY NAME+URL
  6.     - Works recursively (inside all folders)
  7.     - No "Root" folder in output
  8.     - 100% Linux/macOS/Windows compatible (PowerShell 7+)
  9.  
  10. .PARAMETER DedupBy
  11.     'Url'  → remove if same URL
  12.     'Both' → remove only if same URL AND same name
  13. #>
  14.  
  15. [CmdletBinding()]
  16. param(
  17.     [Parameter(Mandatory)] [string] $File1,
  18.     [Parameter(Mandatory)] [string] $File2,
  19.     [Parameter(Mandatory)] [string] $Output,
  20.     [ValidateSet('Url','Both')] [string] $DedupBy = 'Url'
  21. )
  22.  
  23. # ----------------------------------------------------------------------
  24. # Cross-platform HTML encoding
  25. # ----------------------------------------------------------------------
  26. try { $null = [System.Net.WebUtility]::HtmlEncode('') }
  27. catch { Add-Type -AssemblyName System.Net.Http }
  28.  
  29. # ----------------------------------------------------------------------
  30. # Parse HTML → object tree
  31. # ----------------------------------------------------------------------
  32. function ConvertFrom-BookmarkHtml {
  33.     param([string]$Path)
  34.     if (-not (Test-Path $Path)) { Write-Error "Not found: $Path"; return $null }
  35.  
  36.     $content = Get-Content -Path $Path -Raw -Encoding UTF8
  37.     $content = $content -replace "`r`n?", "`n"
  38.  
  39.     $stack = [System.Collections.Stack]::new()
  40.     $root  = [pscustomobject]@{ Type='Folder'; Name='Root'; Children=@() }
  41.     $stack.Push($root)
  42.  
  43.     $reFolderOpen  = [regex]'<DT><H3[^>]*>(?<name>[^<]+)</H3>'
  44.     $reFolderClose = [regex]'</DL>'
  45.     $reLink        = [regex]'<DT><A\s+[^>]*HREF="(?<url>[^"]+)"[^>]*>(?<title>[^<]+)</A>'
  46.  
  47.     foreach ($line in ($content -split "`n")) {
  48.         $line = $line.Trim()
  49.         if ($line -match $reFolderOpen) {
  50.             $f = [pscustomobject]@{ Type='Folder'; Name=$matches['name'].Trim(); Children=@() }
  51.             $stack.Peek().Children += $f
  52.             $stack.Push($f)
  53.         }
  54.         elseif ($line -match $reFolderClose -and $stack.Count -gt 1) {
  55.             $null = $stack.Pop()
  56.         }
  57.         elseif ($line -match $reLink) {
  58.             $b = [pscustomobject]@{ Type='Bookmark'; Name=$matches['title'].Trim(); Url=$matches['url'].Trim() }
  59.             $stack.Peek().Children += $b
  60.         }
  61.     }
  62.     while ($stack.Count -gt 1) { $null = $stack.Pop() }
  63.     return $root
  64. }
  65.  
  66. # ----------------------------------------------------------------------
  67. # Serialize tree → HTML (top-level only)
  68. # ----------------------------------------------------------------------
  69. function ConvertTo-BookmarkHtml {
  70.     param([pscustomobject[]]$Nodes, [int]$Indent=0)
  71.     $pad = ' ' * ($Indent * 2)
  72.     $sb  = [System.Text.StringBuilder]::new()
  73.  
  74.     foreach ($node in $Nodes) {
  75.         if ($node.Type -eq 'Folder') {
  76.             $enc = [System.Net.WebUtility]::HtmlEncode($node.Name)
  77.             [void]$sb.AppendLine("$pad<DT><H3>$enc</H3>")
  78.             [void]$sb.AppendLine("$pad<DL><p>")
  79.             [void]$sb.Append( (ConvertTo-BookmarkHtml $node.Children ($Indent + 1)) )
  80.             [void]$sb.AppendLine("$pad</DL><p>")
  81.         }
  82.         else {
  83.             $n = [System.Net.WebUtility]::HtmlEncode($node.Name)
  84.             $u = [System.Net.WebUtility]::HtmlEncode($node.Url)
  85.             [void]$sb.AppendLine("$pad<DT><A HREF=`"$u`">$n</A>")
  86.         }
  87.     }
  88.     return $sb.ToString()
  89. }
  90.  
  91. # ----------------------------------------------------------------------
  92. # RECURSIVE DEDUPLICATION (inside any folder)
  93. # ----------------------------------------------------------------------
  94. function Remove-Duplicates {
  95.     param([pscustomobject[]]$Bookmarks, [string]$Mode)
  96.     $seen = @{}
  97.  
  98.     $unique = foreach ($bm in $Bookmarks) {
  99.         $key = if ($Mode -eq 'Url') { $bm.Url }
  100.                else { "$($bm.Url)|$($bm.Name)" }
  101.  
  102.         if (-not $seen.ContainsKey($key)) {
  103.             $seen[$key] = $true
  104.             $bm
  105.         }
  106.     }
  107.     return $unique
  108. }
  109.  
  110. # ----------------------------------------------------------------------
  111. # Merge two trees (with recursive deduplication)
  112. # ----------------------------------------------------------------------
  113. function Merge-Trees {
  114.     param(
  115.         [pscustomobject]$Tree1,
  116.         [pscustomobject]$Tree2,
  117.         [string]$DedupBy
  118.     )
  119.  
  120.     # Clone Tree1
  121.     $result = [pscustomobject]@{
  122.         Type     = 'Folder'
  123.         Name     = 'Root'
  124.         Children = @() + $Tree1.Children
  125.     }
  126.  
  127.     # Build folder map
  128.     $folderMap = @{}
  129.     foreach ($f in $result.Children | Where-Object {$_.Type -eq 'Folder'}) {
  130.         $k = $f.Name.ToLowerInvariant()
  131.         if (-not $folderMap.ContainsKey($k)) { $folderMap[$k] = @() }
  132.         $folderMap[$k] += $f
  133.     }
  134.  
  135.     foreach ($node2 in $Tree2.Children) {
  136.         if ($node2.Type -eq 'Bookmark') {
  137.             # Add to result.Children (will be deduplicated later)
  138.             $result.Children += $node2
  139.         }
  140.         else { # Folder
  141.             $k = $node2.Name.ToLowerInvariant()
  142.             if ($folderMap.ContainsKey($k)) {
  143.                 $target = $folderMap[$k][0]
  144.                 $merged = Merge-Trees -Tree1 $target -Tree2 $node2 -DedupBy $DedupBy
  145.                 $target.Children = $merged.Children
  146.             }
  147.             else {
  148.                 $result.Children += $node2
  149.                 $folderMap[$k] = @($node2)
  150.             }
  151.         }
  152.     }
  153.  
  154.     # === RECURSIVE DEDUPLICATION ===
  155.     # Deduplicate bookmarks at this level
  156.     $bookmarks = $result.Children | Where-Object {$_.Type -eq 'Bookmark'}
  157.     $folders   = $result.Children | Where-Object {$_.Type -eq 'Folder'}
  158.  
  159.     $dedupedBookmarks = Remove-Duplicates $bookmarks $DedupBy
  160.  
  161.     # Recurse into folders
  162.     $finalFolders = foreach ($f in $folders) {
  163.         $f.Children = (Merge-Trees -Tree1 ([pscustomobject]@{Children=$f.Children}) -Tree2 ([pscustomobject]@{Children=@()}) -DedupBy $DedupBy).Children
  164.         $f
  165.     }
  166.  
  167.     $result.Children = @() + $dedupedBookmarks + $finalFolders
  168.     return $result
  169. }
  170.  
  171. # ----------------------------------------------------------------------
  172. # Main
  173. # ----------------------------------------------------------------------
  174. try {
  175.     $t1 = ConvertFrom-BookmarkHtml $File1
  176.     $t2 = ConvertFrom-BookmarkHtml $File2
  177.     if (-not $t1 -or -not $t2) { exit 1 }
  178.  
  179.     # Merge + deduplicate recursively
  180.     $merged = Merge-Trees -Tree1 $t1 -Tree2 $t2 -DedupBy $DedupBy
  181.  
  182.     # Output top-level only
  183.     $header = @"
  184. <!DOCTYPE NETSCAPE-Bookmark-file-1>
  185. <!-- Merged by PowerShell -->
  186. <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
  187. <TITLE>Bookmarks</TITLE>
  188. <H1>Bookmarks</H1>
  189.  
  190. <DL><p>
  191. "@
  192.  
  193.     $body   = ConvertTo-BookmarkHtml $merged.Children
  194.     $footer = "</DL><p>"
  195.  
  196.     $final = $header + $body + $footer
  197.  
  198.     $outDir = Split-Path $Output -Parent
  199.     if ($outDir -and -not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null }
  200.  
  201.     $final | Set-Content -Path $Output -Encoding UTF8
  202.     Write-Host "Merged & deduplicated → $Output" -ForegroundColor Green
  203. }
  204. catch {
  205.     Write-Error "Error: $($_.Exception.Message)"
  206.     exit 1
  207. }
Advertisement
Add Comment
Please, Sign In to add comment