1+ //
2+ // Minify.swift
3+ //
4+ //
5+ // Created by Evan Anderson on 3/31/25.
6+ //
7+
8+ import SwiftSyntax
9+
10+ extension HTMLKitUtilities {
11+ static let defaultPreservedWhitespaceTags : Set < String > = Set ( [
12+ " a " , " abbr " ,
13+ " b " , " bdi " , " bdo " , " button " ,
14+ " cite " , " code " ,
15+ " data " , " dd " , " dfn " , " dt " ,
16+ " em " ,
17+ " h1 " , " h2 " , " h3 " , " h4 " , " h5 " , " h6 " ,
18+ " i " ,
19+ " kbd " ,
20+ " label " , " li " ,
21+ " mark " ,
22+ " p " ,
23+ " q " ,
24+ " rp " ,
25+ " rt " ,
26+ " ruby " ,
27+ " s " , " samp " , " small " , " span " , " strong " , " sub " , " sup " ,
28+ " td " , " time " , " title " , " tr " ,
29+ " u " ,
30+ " var " ,
31+ " wbr "
32+ ] . map { " < " + $0 + " > " } )
33+
34+ /// Removes whitespace between elements.
35+ public static func minify(
36+ html: String ,
37+ preservingWhitespaceForTags: Set < String > = [ ]
38+ ) -> String {
39+ var preservedWhitespaceTags : Set < String > = Self . defaultPreservedWhitespaceTags
40+ preservedWhitespaceTags. formUnion ( preservingWhitespaceForTags)
41+ var result : String = " "
42+ result. reserveCapacity ( html. count)
43+ let tagRegex = " [^/>]+ "
44+ let openElementRegex = " (< \( tagRegex) >) "
45+ let openElementRanges = html. ranges ( of: try ! Regex ( openElementRegex) )
46+
47+ let closeElementRegex = " (</ \( tagRegex) >) "
48+ let closeElementRanges = html. ranges ( of: try ! Regex ( closeElementRegex) )
49+
50+ var openingRangeIndex = 0
51+ var ignoredClosingTags : Set < Range < String . Index > > = [ ]
52+ for openingRange in openElementRanges {
53+ let tag = html [ openingRange]
54+ result += tag
55+ let closure : ( Character ) -> Bool = preservedWhitespaceTags. contains ( String ( tag) ) ? { _ in true } : {
56+ !( $0. isWhitespace || $0. isNewline)
57+ }
58+ let closestClosingRange = closeElementRanges. first ( where: { $0. lowerBound > openingRange. upperBound } )
59+ if let nextOpeningRange = openElementRanges. get ( openingRangeIndex + 1 ) {
60+ var i = openingRange. upperBound
61+ var lowerBound = nextOpeningRange. lowerBound
62+ if let closestClosingRange {
63+ if closestClosingRange. upperBound < lowerBound {
64+ lowerBound = closestClosingRange. upperBound
65+ }
66+ if closestClosingRange. lowerBound < nextOpeningRange. lowerBound {
67+ ignoredClosingTags. insert ( closestClosingRange)
68+ }
69+ }
70+ // anything after the opening tag, upto the end of the next closing tag
71+ while i < lowerBound {
72+ let char = html [ i]
73+ if closure ( char) {
74+ result. append ( char)
75+ }
76+ html. formIndex ( after: & i)
77+ }
78+ // anything after the closing tag and before the next opening tag
79+ while i < nextOpeningRange. lowerBound {
80+ let char = html [ i]
81+ if !char. isNewline {
82+ result. append ( char)
83+ }
84+ html. formIndex ( after: & i)
85+ }
86+ } else if let closestClosingRange {
87+ // anything after the opening tag and before the next closing tag
88+ let slice = html [ openingRange. upperBound..< closestClosingRange. lowerBound]
89+ for char in slice {
90+ if closure ( char) {
91+ result. append ( char)
92+ }
93+ }
94+ }
95+ openingRangeIndex += 1
96+ }
97+ for closingRange in closeElementRanges {
98+ if !ignoredClosingTags. contains ( closingRange) {
99+ result += html [ closingRange]
100+ }
101+ }
102+ return result
103+ }
104+ }
0 commit comments