kicad/common/io/kicad/kicad_io_utils.cpp

239 lines
7.7 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <fmt/format.h>
#include <kiid.h>
#include <io/kicad/kicad_io_utils.h>
#include <richio.h>
#include <string_utils.h>
namespace KICAD_FORMAT {
void FormatBool( OUTPUTFORMATTER* aOut, int aNestLevel, const wxString& aKey, bool aValue,
char aSuffix )
{
aOut->Print( aNestLevel, "(%ls %s)%c", aKey.wc_str(), aValue ? "yes" : "no", aSuffix );
}
void FormatUuid( OUTPUTFORMATTER* aOut, const KIID& aUuid, char aSuffix )
{
if( aSuffix )
aOut->Print( 0, "(uuid \"%s\")%c", TO_UTF8( aUuid.AsString() ), aSuffix );
else
aOut->Print( 0, "(uuid \"%s\")", TO_UTF8( aUuid.AsString() ) );
}
/*
* Formatting rules:
* - All extra (non-indentation) whitespace is trimmed
* - Indentation is one tab
* - Starting a new list (open paren) starts a new line with one deeper indentation
* - Lists with no inner lists go on a single line
* - End of multi-line lists (close paren) goes on a single line at same indentation as its start
*
* For example:
* (first
* (second
* (third list)
* (another list)
* )
* (fifth)
* (sixth thing with lots of tokens
* (and a sub list)
* )
* )
*/
void Prettify( std::string& aSource, char aQuoteChar )
{
// Configuration
const char indentChar = '\t';
const int indentSize = 1;
// In order to visually compress PCB files, it is helpful to special-case long lists of (xy ...)
// lists, which we allow to exist on a single line until we reach column 99.
const int xySpecialCaseColumnLimit = 99;
// If whitespace occurs inside a list after this threshold, it will be converted into a newline
// and the indentation will be increased. This is mainly used for image and group objects,
// which contain potentially long sets of string tokens within a single list.
const int consecutiveTokenWrapThreshold = 72;
std::string formatted;
formatted.reserve( aSource.length() );
auto cursor = aSource.begin();
auto seek = cursor;
int listDepth = 0;
char lastNonWhitespace = 0;
bool inQuote = false;
bool hasInsertedSpace = false;
bool inMultiLineList = false;
bool inXY = false;
int column = 0;
int backslashCount = 0; // Count of successive backslash read since any other char
auto isWhitespace = []( const char aChar )
{
return ( aChar == ' ' || aChar == '\t' || aChar == '\n' || aChar == '\r' );
};
auto nextNonWhitespace =
[&]( std::string::iterator aIt )
{
seek = aIt;
while( seek != aSource.end() && isWhitespace( *seek ) )
seek++;
if( seek == aSource.end() )
return (char)0;
return *seek;
};
auto isXY =
[&]( std::string::iterator aIt )
{
seek = aIt;
if( ++seek == aSource.end() || *seek != 'x' )
return false;
if( ++seek == aSource.end() || *seek != 'y' )
return false;
if( ++seek == aSource.end() || *seek != ' ' )
return false;
return true;
};
while( cursor != aSource.end() )
{
char next = nextNonWhitespace( cursor );
if( isWhitespace( *cursor ) && !inQuote )
{
if( !hasInsertedSpace // Only permit one space between chars
&& listDepth > 0 // Do not permit spaces in outer list
&& lastNonWhitespace != '(' // Remove extra space after start of list
&& next != ')' // Remove extra space before end of list
&& next != '(' ) // Remove extra space before newline
{
if( inXY || column < consecutiveTokenWrapThreshold )
{
// Note that we only insert spaces here, no matter what kind of whitespace is in
// the input. Newlines will be inserted as needed by the logic below.
formatted.push_back( ' ' );
column++;
}
else
{
formatted += fmt::format( "\n{}",
std::string( listDepth * indentSize, indentChar ) );
column = listDepth * indentSize;
inMultiLineList = true;
}
hasInsertedSpace = true;
}
}
else
{
hasInsertedSpace = false;
if( *cursor == '(' && !inQuote )
{
bool currentIsXY = isXY( cursor );
if( listDepth == 0 )
{
formatted.push_back( '(' );
column++;
}
else if( inXY && currentIsXY && column < xySpecialCaseColumnLimit )
{
// List-of-points special case
formatted += " (";
column += 2;
inXY = true;
}
else
{
formatted += fmt::format( "\n{}(",
std::string( listDepth * indentSize, indentChar ) );
column = listDepth * indentSize + 1;
}
inXY = currentIsXY;
listDepth++;
}
else if( *cursor == ')' && !inQuote )
{
if( listDepth > 0 )
listDepth--;
if( lastNonWhitespace == ')' || inMultiLineList )
{
formatted += fmt::format( "\n{})",
std::string( listDepth * indentSize, indentChar ) );
column = listDepth * indentSize + 1;
inMultiLineList = false;
}
else
{
formatted.push_back( ')' );
column++;
}
}
else
{
// The output formatter escapes double-quotes (like \")
// But a corner case is a sequence like \\"
// therefore a '\' is attached to a '"' if a odd number of '\' is detected
if( *cursor == '\\' )
backslashCount++;
else if( *cursor == aQuoteChar && ( backslashCount & 1 ) == 0 )
inQuote = !inQuote;
if( *cursor != '\\' )
backslashCount = 0;
formatted.push_back( *cursor );
column++;
}
lastNonWhitespace = *cursor;
}
++cursor;
}
// newline required at end of line / file for POSIX compliance. Keeps git diffs clean.
formatted += '\n';
aSource = std::move( formatted );
}
} // namespace KICAD_FORMAT