KiCad PCB EDA Suite
Loading...
Searching...
No Matches
kicad_io_utils.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#include <fmt/format.h>
21
22#include <kiid.h>
24#include <richio.h>
25#include <string_utils.h>
26
27namespace KICAD_FORMAT {
28
29void FormatBool( OUTPUTFORMATTER* aOut, const wxString& aKey, bool aValue )
30{
31 aOut->Print( "(%ls %s)", aKey.wc_str(), aValue ? "yes" : "no" );
32}
33
34
35void FormatUuid( OUTPUTFORMATTER* aOut, const KIID& aUuid )
36{
37 aOut->Print( "(uuid %s)", aOut->Quotew( aUuid.AsString() ).c_str() );
38}
39
40/*
41 * Formatting rules:
42 * - All extra (non-indentation) whitespace is trimmed
43 * - Indentation is one tab
44 * - Starting a new list (open paren) starts a new line with one deeper indentation
45 * - Lists with no inner lists go on a single line
46 * - End of multi-line lists (close paren) goes on a single line at same indentation as its start
47 *
48 * For example:
49 * (first
50 * (second
51 * (third list)
52 * (another list)
53 * )
54 * (fifth)
55 * (sixth thing with lots of tokens
56 * (and a sub list)
57 * )
58 * )
59 */
60void Prettify( std::string& aSource, bool aCompactSave )
61{
62 // Configuration
63 const char quoteChar = '"';
64 const char indentChar = '\t';
65 const int indentSize = 1;
66
67 // In order to visually compress PCB files, it is helpful to special-case long lists of (xy ...)
68 // lists, which we allow to exist on a single line until we reach column 99.
69 const int xySpecialCaseColumnLimit = 99;
70
71 // If whitespace occurs inside a list after this threshold, it will be converted into a newline
72 // and the indentation will be increased. This is mainly used for image and group objects,
73 // which contain potentially long sets of string tokens within a single list.
74 const int consecutiveTokenWrapThreshold = 72;
75
76 std::string formatted;
77 formatted.reserve( aSource.length() );
78
79 auto cursor = aSource.begin();
80 auto seek = cursor;
81
82 int listDepth = 0;
83 char lastNonWhitespace = 0;
84 bool inQuote = false;
85 bool hasInsertedSpace = false;
86 bool inMultiLineList = false;
87 bool inXY = false;
88 bool inShortForm = false;
89 int shortFormDepth = 0;
90 int column = 0;
91 int backslashCount = 0; // Count of successive backslash read since any other char
92
93 auto isWhitespace = []( const char aChar )
94 {
95 return ( aChar == ' ' || aChar == '\t' || aChar == '\n' || aChar == '\r' );
96 };
97
98 auto nextNonWhitespace =
99 [&]( std::string::iterator aIt )
100 {
101 seek = aIt;
102
103 while( seek != aSource.end() && isWhitespace( *seek ) )
104 seek++;
105
106 if( seek == aSource.end() )
107 return (char)0;
108
109 return *seek;
110 };
111
112 auto isXY =
113 [&]( std::string::iterator aIt )
114 {
115 seek = aIt;
116
117 if( ++seek == aSource.end() || *seek != 'x' )
118 return false;
119
120 if( ++seek == aSource.end() || *seek != 'y' )
121 return false;
122
123 if( ++seek == aSource.end() || *seek != ' ' )
124 return false;
125
126 return true;
127 };
128
129 auto isShortForm =
130 [&]( std::string::iterator aIt )
131 {
132 seek = aIt;
133 std::string token;
134
135 while( ++seek != aSource.end() && isalpha( *seek ) )
136 token += *seek;
137
138 return token == "font" || token == "stroke" || token == "fill"
139 || token == "offset" || token == "rotate" || token == "scale";
140 };
141
142 while( cursor != aSource.end() )
143 {
144 char next = nextNonWhitespace( cursor );
145
146 if( isWhitespace( *cursor ) && !inQuote )
147 {
148 if( !hasInsertedSpace // Only permit one space between chars
149 && listDepth > 0 // Do not permit spaces in outer list
150 && lastNonWhitespace != '(' // Remove extra space after start of list
151 && next != ')' // Remove extra space before end of list
152 && next != '(' ) // Remove extra space before newline
153 {
154 if( inXY || column < consecutiveTokenWrapThreshold )
155 {
156 // Note that we only insert spaces here, no matter what kind of whitespace is
157 // in the input. Newlines will be inserted as needed by the logic below.
158 formatted.push_back( ' ' );
159 column++;
160 }
161 else if( inShortForm )
162 {
163 formatted.push_back( ' ' );
164 }
165 else
166 {
167 formatted += fmt::format( "\n{}",
168 std::string( listDepth * indentSize, indentChar ) );
169 column = listDepth * indentSize;
170 inMultiLineList = true;
171 }
172
173 hasInsertedSpace = true;
174 }
175 }
176 else
177 {
178 hasInsertedSpace = false;
179
180 if( *cursor == '(' && !inQuote )
181 {
182 bool currentIsXY = isXY( cursor );
183 bool currentIsShortForm = aCompactSave && isShortForm( cursor );
184
185 if( formatted.empty() )
186 {
187 formatted.push_back( '(' );
188 column++;
189 }
190 else if( inXY && currentIsXY && column < xySpecialCaseColumnLimit )
191 {
192 // List-of-points special case
193 formatted += " (";
194 column += 2;
195 }
196 else if( inShortForm )
197 {
198 formatted += " (";
199 column += 2;
200 }
201 else
202 {
203 formatted += fmt::format( "\n{}(",
204 std::string( listDepth * indentSize, indentChar ) );
205 column = listDepth * indentSize + 1;
206 }
207
208 inXY = currentIsXY;
209
210 if( currentIsShortForm )
211 {
212 inShortForm = true;
213 shortFormDepth = listDepth;
214 }
215
216 listDepth++;
217 }
218 else if( *cursor == ')' && !inQuote )
219 {
220 if( listDepth > 0 )
221 listDepth--;
222
223 if( inShortForm )
224 {
225 formatted.push_back( ')' );
226 column++;
227 }
228 else if( lastNonWhitespace == ')' || inMultiLineList )
229 {
230 formatted += fmt::format( "\n{})",
231 std::string( listDepth * indentSize, indentChar ) );
232 column = listDepth * indentSize + 1;
233 inMultiLineList = false;
234 }
235 else
236 {
237 formatted.push_back( ')' );
238 column++;
239 }
240
241 if( shortFormDepth == listDepth )
242 {
243 inShortForm = false;
244 shortFormDepth = 0;
245 }
246 }
247 else
248 {
249 // The output formatter escapes double-quotes (like \")
250 // But a corner case is a sequence like \\"
251 // therefore a '\' is attached to a '"' if a odd number of '\' is detected
252 if( *cursor == '\\' )
253 backslashCount++;
254 else if( *cursor == quoteChar && ( backslashCount & 1 ) == 0 )
255 inQuote = !inQuote;
256
257 if( *cursor != '\\' )
258 backslashCount = 0;
259
260 formatted.push_back( *cursor );
261 column++;
262 }
263
264 lastNonWhitespace = *cursor;
265 }
266
267 ++cursor;
268 }
269
270 // newline required at end of line / file for POSIX compliance. Keeps git diffs clean.
271 formatted += '\n';
272
273 aSource = std::move( formatted );
274}
275
276} // namespace KICAD_FORMAT
Definition: kiid.h:49
wxString AsString() const
Definition: kiid.cpp:238
An interface used to output 8 bit text in a convenient way.
Definition: richio.h:322
std::string Quotew(const wxString &aWrapee) const
Definition: richio.cpp:543
int PRINTF_FUNC_N Print(int nestLevel, const char *fmt,...)
Format and write text to the output stream.
Definition: richio.cpp:458
void Prettify(std::string &aSource, bool aCompactSave)
void FormatUuid(OUTPUTFORMATTER *aOut, const KIID &aUuid)
void FormatBool(OUTPUTFORMATTER *aOut, const wxString &aKey, bool aValue)
Writes a boolean to the formatter, in the style (aKey [yes|no])
CITER next(CITER it)
Definition: ptree.cpp:126