File: ConvertCase.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (157 lines) | stat: -rw-r--r-- 5,710 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
enum ConvertCase {

    static func witIdentifier(identifier: [String]) -> String {
        return witIdentifier(kebabCase(identifier: identifier))
    }

    static func witIdentifier(identifier: String) -> String {
        return witIdentifier(kebabCase(identifier: identifier))
    }

    static func witIdentifier(_ id: String) -> String {
        // https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md#keywords
        let keywords: Set<String> = [
            "use",
            "type",
            "resource",
            "func",
            "record",
            "enum",
            "flags",
            "variant",
            "static",
            "interface",
            "world",
            "import",
            "export",
            "package",
            "include",
        ]

        if keywords.contains(id) {
            return "%\(id)"
        }
        return id
    }

    static func kebabCase(identifier: [String]) -> String {
        identifier.map { kebabCase(identifier: $0) }.joined(separator: "-")
    }

    /// Convert any Swift-like identifier to WIT identifier
    ///
    /// The WIT identifier is defined as follows:
    ///
    /// ```
    /// label          ::= <word>
    ///                  | <label>-<word>
    /// word           ::= [a-z][0-9a-z]*
    ///                  | [A-Z][0-9A-Z]*
    /// ```
    /// > See <https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#instance-definitions>
    ///
    /// Note that different inputs can produce the same output.
    static func kebabCase(identifier: String) -> String {
        struct Word {
            var text: String
            let isUpperCases: Bool
        }
        var words: [Word] = []
        var cursor = identifier.startIndex

        let lowerCases: ClosedRange<Character> = "a"..."z"
        let upperCases: ClosedRange<Character> = "A"..."Z"
        let digits: ClosedRange<Character> = "0"..."9"

        var nextChar: Character? {
            let nextCursor = identifier.index(after: cursor)
            guard identifier.index(after: cursor) < identifier.endIndex else {
                return nil
            }
            return identifier[nextCursor]
        }
        var char: Character { identifier[cursor] }

        // 1. Split into words by following the definition
        while cursor < identifier.endIndex {
            // Start of a "word"

            var isUpperCases: Bool
            var building = ""

            // Consume [A-Z]
            // Note that it doesn't consume [A-Z][0-9A-Z]* here to allow later heuristic word merging.
            if upperCases.contains(char) {
                isUpperCases = true
                building.append(char)
                cursor = identifier.index(after: cursor)
            } else if lowerCases.contains(char) {
                isUpperCases = false
                // Consume [a-z][0-9a-z]*
                while cursor < identifier.endIndex, lowerCases.contains(char) || digits.contains(char) {
                    building.append(char)
                    cursor = identifier.index(after: cursor)
                }
            } else {
                // Otherwise, the char appears invalid position or the char itself is invalid.
                // If the char itself is valid, append it at the tail of the last word
                if digits.contains(char), let lastWord = words.popLast() {
                    building = lastWord.text + String(char)
                    isUpperCases = lastWord.isUpperCases
                } else {
                    // Just ignore the char if it's invalid char
                    cursor = identifier.index(after: cursor)
                    continue
                }
                cursor = identifier.index(after: cursor)
            }
            if !building.isEmpty {
                words.append(Word(text: building, isUpperCases: isUpperCases))
                building = ""
            }
        }

        // 2. Merge words by some heuristics
        var mergedWords: [Word] = []

        // Merge Pascal case words into all lower-cased word
        do {
            var wordIndex = 0
            while wordIndex < words.count - 1 {
                let word = words[wordIndex]
                let nextWord = words[wordIndex + 1]

                // Merge ["P", "ascal", "C", "ase"] -> ["pascal", "case"]
                if word.text.count == 1, word.isUpperCases, !nextWord.isUpperCases {
                    mergedWords.append(Word(text: word.text.lowercased() + nextWord.text, isUpperCases: false))
                    wordIndex += 1
                } else {
                    mergedWords.append(word)
                }
                wordIndex += 1
            }
            // Append the trailing word if it's not merged
            if wordIndex == words.count - 1 {
                mergedWords.append(words[wordIndex])
            }
        }

        // Merge trailing upper cases like ["mac", "O", "S"] -> ["mac", "OS"]
        // but it doesn't merge non-trailing upper words like ["C", "Language"]
        do {
            while mergedWords.count >= 2,
                let lastWord = mergedWords.popLast(),
                let nextLastWord = mergedWords.popLast()
            {
                if lastWord.isUpperCases, nextLastWord.isUpperCases {
                    mergedWords.append(Word(text: nextLastWord.text + lastWord.text, isUpperCases: true))
                } else {
                    mergedWords.append(nextLastWord)
                    mergedWords.append(lastWord)
                    break
                }
            }
        }
        return mergedWords.map(\.text).joined(separator: "-")
    }
}