/* * 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 . */ #include #include #include #include #include namespace KICAD_FORMAT { void FormatBool( OUTPUTFORMATTER* aOut, int aNestLevel, const wxString& aKey, bool aValue, char aSuffix ) { if( aNestLevel ) aOut->Print( aNestLevel, "(%ls %s)", aKey.wc_str(), aValue ? "yes" : "no" ); else aOut->Print( 0, " (%ls %s)", aKey.wc_str(), aValue ? "yes" : "no" ); if( aSuffix ) aOut->Print( 0, "%c", aSuffix ); } void FormatUuid( OUTPUTFORMATTER* aOut, int aNestLevel, const KIID& aUuid, char aSuffix ) { if( aNestLevel ) aOut->Print( aNestLevel, "(uuid \"%s\")", TO_UTF8( aUuid.AsString() ) ); else aOut->Print( 0, " (uuid \"%s\")", TO_UTF8( aUuid.AsString() ) ); if( aSuffix ) aOut->Print( 0, "%c", aSuffix ); } /* * 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