1 | /* |
2 | SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org> |
3 | SPDX-License-Identifier: LGPL-2.0-or-later |
4 | */ |
5 | |
6 | #include "addressformatter_p.h" |
7 | |
8 | #include "address.h" |
9 | #include "addressformat.h" |
10 | #include "addressformat_p.h" |
11 | #include "addressformatscript_p.h" |
12 | |
13 | #include <KCountry> |
14 | |
15 | #include <QDebug> |
16 | #include <QStringList> |
17 | |
18 | using namespace KContacts; |
19 | |
20 | static constexpr auto AllFields = AddressFormatField::Country | AddressFormatField::Region | AddressFormatField::Locality |
21 | | AddressFormatField::DependentLocality | AddressFormatField::SortingCode | AddressFormatField::PostalCode | AddressFormatField::StreetAddress |
22 | | AddressFormatField::Organization | AddressFormatField::Name | AddressFormatField::PostOfficeBox; |
23 | static constexpr auto AllDomesticFields = AllFields & ~(int)AddressFormatField::Country; |
24 | static constexpr auto GeoUriFields = AddressFormatField::StreetAddress | AddressFormatField::PostalCode | AddressFormatField::Locality |
25 | | AddressFormatField::DependentLocality | AddressFormatField::Region | AddressFormatField::Country; |
26 | |
27 | enum Separator { Newline, Comma, Native }; |
28 | |
29 | // keep the same order as the enum! |
30 | struct { |
31 | Separator separator; |
32 | bool honorUpper; |
33 | bool forceCountry; |
34 | AddressFormatFields includeFields; |
35 | } static constexpr const style_map[] = { |
36 | {.separator: Newline, .honorUpper: true, .forceCountry: true, .includeFields: AllDomesticFields}, // AddressFormatStyle::Postal |
37 | {.separator: Newline, .honorUpper: false, .forceCountry: false, .includeFields: AllDomesticFields}, // AddressFormatStyle::MultiLineDomestic |
38 | {.separator: Newline, .honorUpper: false, .forceCountry: true, .includeFields: AllFields}, // AddressFormatStyle::MultiLineInternational |
39 | {.separator: Native, .honorUpper: false, .forceCountry: false, .includeFields: AllDomesticFields}, // AddressFormatStyle::SingleLineDomestic |
40 | {.separator: Native, .honorUpper: false, .forceCountry: true, .includeFields: AllFields}, // AddressFormatStyle::SingleLineInternational |
41 | {.separator: Comma, .honorUpper: true, .forceCountry: true, .includeFields: GeoUriFields}, // AddressFormatStyle::GeoUriQuery |
42 | }; |
43 | |
44 | static constexpr const char *separator_map[] = {"\n" , "," }; |
45 | static constexpr const char *native_separator_map[] = {", " , "، " , "" , " " }; |
46 | |
47 | static bool isReverseOrder(const AddressFormat &fmt) |
48 | { |
49 | return !fmt.elements().empty() && fmt.elements()[0].field() == AddressFormatField::Country; |
50 | } |
51 | |
52 | QString |
53 | AddressFormatter::format(const Address &address, const QString &name, const QString &organization, const AddressFormat &format, AddressFormatStyle style) |
54 | { |
55 | const auto styleData = style_map[(int)style]; |
56 | const auto isFieldEmpty = [&](AddressFormatField f) -> bool { |
57 | if ((styleData.includeFields & f) == 0) { |
58 | return true; |
59 | } |
60 | switch (f) { |
61 | case AddressFormatField::NoField: |
62 | case AddressFormatField::DependentLocality: |
63 | case AddressFormatField::SortingCode: |
64 | return true; |
65 | case AddressFormatField::Name: |
66 | return name.isEmpty(); |
67 | case AddressFormatField::Organization: |
68 | return organization.isEmpty(); |
69 | case AddressFormatField::PostOfficeBox: |
70 | return address.postOfficeBox().isEmpty(); |
71 | case AddressFormatField::StreetAddress: |
72 | return address.street().isEmpty() && (address.extended().isEmpty() || style == AddressFormatStyle::GeoUriQuery); |
73 | case AddressFormatField::PostalCode: |
74 | return address.postalCode().isEmpty(); |
75 | case AddressFormatField::Locality: |
76 | return address.locality().isEmpty(); |
77 | case AddressFormatField::Region: |
78 | return address.region().isEmpty(); |
79 | case AddressFormatField::Country: |
80 | return address.country().isEmpty(); |
81 | } |
82 | return true; |
83 | }; |
84 | const auto countryName = [&]() -> QString { |
85 | if (address.country().isEmpty()) { |
86 | return {}; |
87 | } |
88 | // we use the already ISO 3166-1 resolved country from format here to |
89 | // avoid a potentially expensive second name-based lookup |
90 | return style == AddressFormatStyle::GeoUriQuery ? format.country() : KCountry::fromAlpha2(alpha2Code: format.country()).name(); |
91 | }; |
92 | |
93 | QStringList lines; |
94 | QString line, secondaryLine; |
95 | |
96 | for (auto it = format.elements().begin(); it != format.elements().end(); ++it) { |
97 | // add separators if: |
98 | // - the preceding line is not empty |
99 | // - we use newline separators and the preceding element is another separator |
100 | const auto precedingSeparator = (it != format.elements().begin() && (*std::prev(x: it)).isSeparator()); |
101 | if ((*it).isSeparator() && (!line.isEmpty() || (precedingSeparator && styleData.separator == Newline))) { |
102 | lines.push_back(t: line); |
103 | line.clear(); |
104 | if (!secondaryLine.isEmpty()) { |
105 | lines.push_back(t: secondaryLine); |
106 | secondaryLine.clear(); |
107 | } |
108 | continue; |
109 | } |
110 | |
111 | // literals are only added if they not follow an empty field and are not preceding an empty field |
112 | // to support incomplete addresses we deviate from the libaddressinput algorithm here and also add |
113 | // the separator if any preceding field in the same line is non-empty, not just the immediate one. |
114 | // this is to produce useful output e.g. for "%C %S %Z" if %S is empty. |
115 | bool precedingFieldHasContent = (it == format.elements().begin() || (*std::prev(x: it)).isSeparator()); |
116 | for (auto it2 = it; !(*it2).isSeparator(); --it2) { |
117 | if ((*it2).isField() && !isFieldEmpty((*it2).field())) { |
118 | precedingFieldHasContent = true; |
119 | break; |
120 | } |
121 | if (it2 == format.elements().begin()) { |
122 | break; |
123 | } |
124 | } |
125 | const auto followingFieldEmpty = (std::next(x: it) != format.elements().end() && (*std::next(x: it)).isField() && isFieldEmpty((*std::next(x: it)).field())); |
126 | if ((*it).isLiteral() && precedingFieldHasContent && !followingFieldEmpty) { |
127 | line += (*it).literal(); |
128 | continue; |
129 | } |
130 | |
131 | if ((*it).isField() && (styleData.includeFields & (*it).field())) { |
132 | QString v; |
133 | switch ((*it).field()) { |
134 | case AddressFormatField::NoField: |
135 | case AddressFormatField::DependentLocality: |
136 | case AddressFormatField::SortingCode: |
137 | break; |
138 | case AddressFormatField::Name: |
139 | v = name; |
140 | break; |
141 | case AddressFormatField::Organization: |
142 | v = organization; |
143 | break; |
144 | case AddressFormatField::PostOfficeBox: |
145 | v = address.postOfficeBox(); |
146 | break; |
147 | case AddressFormatField::StreetAddress: |
148 | if (!address.street().isEmpty() && !address.extended().isEmpty() && style != AddressFormatStyle::GeoUriQuery) { |
149 | if (isReverseOrder(fmt: format)) { |
150 | secondaryLine = address.extended(); |
151 | } else { |
152 | lines.push_back(t: address.extended()); |
153 | } |
154 | } |
155 | v = address.street().isEmpty() ? address.extended() : address.street(); |
156 | break; |
157 | case AddressFormatField::PostalCode: |
158 | v = address.postalCode(); |
159 | break; |
160 | case AddressFormatField::Locality: |
161 | v = address.locality(); |
162 | break; |
163 | case AddressFormatField::Region: |
164 | v = address.region(); |
165 | break; |
166 | case AddressFormatField::Country: |
167 | v = countryName(); |
168 | break; |
169 | } |
170 | if (styleData.honorUpper && format.upperCaseFields() & (*it).field()) { |
171 | v = v.toUpper(); |
172 | } |
173 | line += v; |
174 | } |
175 | } |
176 | if (!line.isEmpty()) { |
177 | lines.push_back(t: line); |
178 | } |
179 | if (!secondaryLine.isEmpty()) { |
180 | lines.push_back(t: secondaryLine); |
181 | } |
182 | |
183 | // append country for formats that need it (international style + not yet present in format.elements()) |
184 | if (styleData.forceCountry && (format.usedFields() & AddressFormatField::Country & styleData.includeFields) == 0 && !address.country().isEmpty()) { |
185 | auto c = countryName(); |
186 | if (style == AddressFormatStyle::Postal) { |
187 | // the format of the country for postal addresses depends on the sending country, not the destination |
188 | const auto sourceCountry = KCountry::fromQLocale(country: QLocale().territory()); |
189 | const auto sourceFmt = AddressFormatRepository::formatForCountry(countryCode: sourceCountry.alpha2(), scriptPref: AddressFormatScriptPreference::Local); |
190 | const auto shouldPrepend = isReverseOrder(fmt: sourceFmt); |
191 | if (!lines.isEmpty()) { |
192 | shouldPrepend ? lines.push_front(t: {}) : lines.push_back(t: {}); |
193 | } |
194 | if (styleData.honorUpper && (sourceFmt.upperCaseFields() & AddressFormatField::Country)) { |
195 | c = c.toUpper(); |
196 | } |
197 | shouldPrepend ? lines.push_front(t: c) : lines.push_back(t: c); |
198 | } else { |
199 | if (styleData.honorUpper && (format.upperCaseFields() & AddressFormatField::Country)) { |
200 | c = c.toUpper(); |
201 | } |
202 | lines.push_back(t: c); |
203 | } |
204 | } |
205 | |
206 | if (styleData.separator == Native) { |
207 | const auto script = AddressFormatScript::detect(addr: address); |
208 | return lines.join(sep: QString::fromUtf8(utf8: native_separator_map[script])); |
209 | } |
210 | return lines.join(sep: QLatin1String(separator_map[styleData.separator])); |
211 | } |
212 | |