Support multi-column output.

This commit is contained in:
John Sennesael 2022-03-19 16:16:20 -05:00
parent 3b36b76ef6
commit 5eb93bce96
21 changed files with 472 additions and 84 deletions

View File

@ -1,4 +1,4 @@
# Copyright© 2021 John Sennesael
# Copyright© 2022 John Sennesael
#
# This file is part of justify.
#
@ -29,6 +29,7 @@ set(CMAKE_CXX_STANDARD 20)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
add_executable(justify
"src/columns.cpp"
"src/justify.cpp"
"src/justifyCenter.cpp"
"src/justifyCommon.cpp"

View File

@ -46,3 +46,14 @@ The standard cmake routine:
Justify method (default=fill).
-h,--help Show this help text.
## Screenshots
![2 column output example](./justify_2_column.png)
![3 column output example](./justify_3_column.png)
![2 column output with 5 character spacing example](./justify_2_column_5_char_spacing.png)
![2 column output with max. height of 20 characters per block example](./justify_2_column_max_20_tall.png)

BIN
justify_2_column.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
justify_3_column.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

184
src/columns.cpp Normal file
View File

@ -0,0 +1,184 @@
#include "columns.h"
#include "justifyCommon.h"
#include "stringHelper.h"
#include <array>
#include <memory>
#include <string>
#include <vector>
/*
Possible layout and what the variables mean:
columnWidth columnHorizontalSpacing
/ \ /\
/ \/ \
BLA BLA BLAAA ASDFASDFASDFA \
BLA BLA BLA.. ASDF ASDF ASD \ row = a vector of columns that all start
BLABLABLABLA. ASDASDASDASDA / on the same line, but may end on
BLABLA FILLERTEXTLAL / a different line.
\ / ASDASD /
\ / \
block \ columnVerticalSpacing
/
MORE TEXT !!! FOO FOO BAR.. \
ASDF ASDF ASD Fin. \ another row
ASD FOO. /
An individual row can not contain more LINES than maxColumnHeight.
*/
// A block is a set of lines.
using block = std::vector<std::wstring>;
// ... but we also deal with sets of blocks. (eg, for representing a row)
using blocks = std::vector<block>;
std::vector<std::wstring> columns(
const std::vector<std::wstring>& in,
size_t numCols,
size_t columnWidth,
size_t columnHorizontalSpacing,
size_t columnVerticalSpacing,
size_t maxColumnHeight,
size_t width)
{
std::vector<std::wstring> result;
// Basic sanity check.
if ((columnWidth == 0) || (maxColumnHeight == 0)) return result;
// Puncutation/characters that we can potentially consider a good spot
// to start a new block on if a line ends in it.
static std::array<wchar_t, 10> const blockEnders = {
'!', '?', '.', ';', '}', ']', '>'
};
// The input text (in variable) should already be justified and have a
// max text width per line of columnWidth.
// We are going to iterate though it, and break it up in blocks.
// We dont't always break on exactly maxColumnHeight, instead we try
// to find a natural spot NEAR the maxColumnHeight, such as at the end
// of a paragraph or at the end of a sentence.
block currentBlock;
blocks allBlocks;
for(auto lineIt = in.begin(); lineIt != in.end(); ++lineIt)
{
std::wstring line = *lineIt;
std::wstring nextLine;
auto itNext = lineIt + 1;
if (itNext != in.end())
{
nextLine = *itNext;
}
// The block needs to be exactly columnWidth wide to ensure alignment
// is correct when we add the hspacing + next column. If the line is
// shorter, we have to pad with spaces.
while (line.length() < columnWidth)
{
line += L" ";
}
currentBlock.emplace_back(line);
// How far away from the bottom (=max height)?
const int distanceToMaxHeight = maxColumnHeight - currentBlock.size();
// If we have more than maxColumnHeight lines, it's time to start a new
// block no matter what.
if (distanceToMaxHeight <= 0)
{
allBlocks.emplace_back(currentBlock);
currentBlock.clear();
continue;
}
// Otherwise, are we past 2/3 down?
if (distanceToMaxHeight < (maxColumnHeight / 3))
{
// If the current or next line starts a new paragraph, let's just
// start a new block here.
if (line.empty() || nextLine.empty())
{
allBlocks.emplace_back(currentBlock);
currentBlock.clear();
continue;
}
}
// Still here? ok... are we 1/4th of the way down?
if (distanceToMaxHeight < (maxColumnHeight / 4))
{
// We're almost to the end, we should probably start looking for a
// good spot to break now if we haven't found a new paragraph yet.
// Any line ending with punctuation would be good.
const wchar_t lastChar = line[line.size() - 1];
if (std::any_of(blockEnders.begin(), blockEnders.end(),
[&lastChar](wchar_t c){
return c == lastChar;
}))
{
allBlocks.emplace_back(currentBlock);
currentBlock.clear();
continue;
}
}
// If we're still here, we'll just keep iterating until we hit the
// maximum column height.
}
// Cool! we have blocks now - now we have to arrange them into ROWS.
// Each row will have N columns where
// N = width - columnHorizontalSpacing / columnWidth
blocks::iterator curBlockIt = allBlocks.begin();
while (curBlockIt != allBlocks.end())
{
// Make a vector with all the blocks that will make up the current row.
blocks blocksThisRow;
for (size_t i = 0 ; i != numCols; ++i)
{
blocksThisRow.emplace_back(*curBlockIt);
++curBlockIt;
if (curBlockIt == allBlocks.end()) break;
}
// Figure out how tall this row is going to be (how many lines), by
// looking for the biggest block int the vector we just made.
size_t curRowHeight{0};
for (auto& aBlock: blocksThisRow)
{
if (aBlock.size() > curRowHeight) curRowHeight = aBlock.size();
}
// We also add the vertical spacing here to append blank rows in the
// loop below.
curRowHeight += columnVerticalSpacing;
// For every line (0 to curRowHeight), grab a piece out of each block.
std::wstring outLine;
for (size_t nLine = 0; nLine != curRowHeight; ++nLine)
{
for (auto& iBlock: blocksThisRow)
{
if (iBlock.size() < nLine + 1)
{
// If this block has no content for this line,
// fill with spaces instead.
outLine.append(columnWidth + columnHorizontalSpacing, ' ');
}
else
{
outLine += iBlock[nLine];
outLine.append(columnHorizontalSpacing, ' ');
}
}
// We'll have leftover spacing on the right, trim it off.
outLine = StringRightTrim(outLine);
result.emplace_back(outLine);
outLine.clear();
}
}
return result;
}

32
src/columns.h Normal file
View File

@ -0,0 +1,32 @@
/*
Copyright© 2022 John Sennesael
This file is part of justify.
Justify 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.
Justify 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 Justify. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <memory>
#include <string>
#include <vector>
std::vector<std::wstring> columns(
const std::vector<std::wstring>& in,
size_t numCols,
size_t columnWidth,
size_t columnHorizontalSpacing,
size_t columnVerticalSpacing,
size_t maxColumnHeight,
size_t width);

View File

@ -1,5 +1,5 @@
/*
Copyright© 2021 John Sennesael
Copyright© 2022 John Sennesael
This file is part of justify.
Justify is free software: you can redistribute it and/or modify it under the
@ -18,6 +18,7 @@
#include "justify.h"
#include "columns.h"
#include "justifyCenter.h"
#include "justifyFill.h"
#include "justifyLeft.h"
@ -36,23 +37,46 @@
void usage(const std::string& program)
{
std::cout << std::endl;
std::cout << "justify - simple text alignment tool." << std::endl;
std::cout << "Copyright© 2021 John Sennesael" << std::endl << std::endl;
std::cout << "Usage: " << program << " [OPTION]..." << std::endl;
std::cout << std::endl;
std::cout << " -i,--input [FILE] Input file to process, defaults to "
<< "stdin." << std::endl;
std::cout << " -o,--output [FILE] Output file, defaults to stdout."
<< std::endl;
std::cout << " -c,--cols [NUM] Text width to format text into."
<< std::endl
<< " (defaults to terminal width)"
<< std::endl;
std::cout << std::endl << "Input / output options:"
<< std::endl << std::endl;
std::cout << " -i,--input [FILE] Input file to process, defaults to "
<< "stdin." << std::endl;
std::cout << " -o,--output [FILE] Output file, defaults to stdout."
<< std::endl;
std::cout << std::endl << "Column options:" << std::endl << std::endl;
std::cout << " -c,--cols [COLS] Number of columns for multi-column"
<< std::endl
<< " output." << std::endl;
std::cout << " -h,--hspacing [COLS] Column horizontal spacing."
<< std::endl
<< " (default: 2)" << std::endl;
std::cout << " -v,--vspacing [ROWS] Column vertical spacing."
<< std::endl
<< " (default: 1)" << std::endl;
std::cout << " -m,--colheight [ROWS] Column max. height."
<< std::endl
<< " (default: 0, =infinite)"
<< std::endl;
std::cout << std::endl << "Alignment options: "
<< std::endl << std::endl;
std::cout << " -w,--width [NUM] Text width to format text into."
<< std::endl
<< " (defaults to terminal width)"
<< std::endl;
std::cout << " -a,--align [center|fill|left|right]" << std::endl
<< " Justify method (default=fill)."
<< std::endl;
std::cout << " -h,--help Show this help text."
<< std::endl;
<< " Justify method (default=fill)."
<< std::endl;
std::cout << std::endl << "Misc options: " << std::endl << std::endl;
std::cout << " -h,--help Show this help text."
<< std::endl << std::endl;
}
bool checkParam(
@ -107,6 +131,54 @@ Settings parseArgs(const StrVector& args)
}
continue;
}
else if ((arg == "-h") || (arg == "--hspacing"))
{
if (!checkParam("number", arg, args, it)) return result;
try
{
result.hspacing = std::stol(*it);
}
catch (const std::exception& e)
{
std::cerr << "Could not parse " << arg << " as a number: ";
std::cerr << e.what() << std::endl;
usage(args[0]);
return result;
}
continue;
}
else if ((arg == "-v") || (arg == "--vspacing"))
{
if (!checkParam("number", arg, args, it)) return result;
try
{
result.vspacing = std::stol(*it);
}
catch (const std::exception& e)
{
std::cerr << "Could not parse " << arg << " as a number: ";
std::cerr << e.what() << std::endl;
usage(args[0]);
return result;
}
continue;
}
else if ((arg == "-m") || (arg == "--colheight"))
{
if (!checkParam("number", arg, args, it)) return result;
try
{
result.maxColHeight = std::stol(*it);
}
catch (const std::exception& e)
{
std::cerr << "Could not parse " << arg << " as a number: ";
std::cerr << e.what() << std::endl;
usage(args[0]);
return result;
}
continue;
}
else if ((arg == "-a") || (arg == "--align"))
{
if (!checkParam("justification type", arg, args, it))
@ -143,6 +215,22 @@ Settings parseArgs(const StrVector& args)
result.shouldExit = true;
return result;
}
else if ((arg == "-w") || (arg == "--width"))
{
if (!checkParam("number", arg, args, it)) return result;
try
{
result.width = std::stol(*it);
}
catch (const std::exception& e)
{
std::cerr << "Could not parse " << arg << " as a number: ";
std::cerr << e.what() << std::endl;
usage(args[0]);
return result;
}
continue;
}
else
{
std::cerr << "Unknown option: " << arg << std::endl;
@ -215,20 +303,69 @@ bool justify(const Settings& settings)
return false;
}
}
size_t justifyWidth = settings.width;
if (settings.cols > 1)
{
if (settings.hspacing >= settings.width)
{
std::cerr << "Error: Horizontal column spacing too big for given "
<< "width." << std::endl;
return false;
}
// Figure out the width of a column if we have columns.
justifyWidth = settings.width / settings.cols;
justifyWidth -= (settings.cols - 1) * settings.hspacing;
}
std::vector<std::wstring> justifiedText;
switch (settings.justify)
{
case Justification::FILL:
return justifyFill(inputStream, outputStream, settings.cols);
justifiedText = justifyFill(inputStream, justifyWidth);
break;
case Justification::CENTER:
return justifyCenter(inputStream, outputStream, settings.cols);
justifiedText = justifyCenter(inputStream, justifyWidth);
break;
case Justification::RIGHT:
return justifyRight(inputStream, outputStream, settings.cols);
justifiedText = justifyRight(inputStream, justifyWidth);
break;
case Justification::LEFT:
return justifyLeft(inputStream, outputStream, settings.cols);
justifiedText = justifyLeft(inputStream, justifyWidth);
break;
default:
std::cerr << "Not implemented." << std::endl;
return false;
};
std::vector<std::wstring> output;
if (settings.cols > 1)
{
// unless overridden by the user, the column height will be the number
// of justified lines divided by the number of desired columns.
size_t maxColHeight = settings.maxColHeight;
if (maxColHeight == 0)
{
maxColHeight = justifiedText.size() / settings.cols;
}
// Split justified text up into columns.
output = columns(
justifiedText,
settings.cols,
justifyWidth,
settings.hspacing,
settings.vspacing,
maxColHeight,
settings.width
);
}
else
{
output = justifiedText;
}
// Print the result to the output stream.
for (auto& line: output)
{
*outputStream << line << std::endl;
}
return true;
}
int main(int argc, char* argv[])
@ -237,14 +374,23 @@ int main(int argc, char* argv[])
Settings settings = parseArgs(arguments);
if (settings.error) return 1;
if (settings.shouldExit) return 0;
if (settings.cols == 0)
if (settings.width == 0)
{
// Get terminal width (to use as a default)
/// @TODO make sure this works on most unixes
winsize termSize;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &termSize);
settings.cols = termSize.ws_col;
settings.width = termSize.ws_col;
}
return justify(settings) == true ? 0 : 1;
int retval{0};
try {
retval = justify(settings) == true ? 0 : 1;
}
catch (const std::exception& e)
{
std::cerr << "Error: " << e.what() << std::endl;
retval = 2;
}
return retval;
}

View File

@ -1,5 +1,5 @@
/*
Copyright© 2021 John Sennesael
Copyright© 2022 John Sennesael
This file is part of justify.
Justify is free software: you can redistribute it and/or modify it under the
@ -29,8 +29,12 @@ struct Settings
{
size_t cols{};
bool error{false};
bool shouldExit{false};
size_t hspacing{2};
std::string inFile;
std::string outFile;
Justification justify{Justification::FILL};
size_t maxColHeight{};
std::string outFile;
bool shouldExit{false};
size_t vspacing{1};
size_t width{};
};

View File

@ -1,5 +1,5 @@
/*
Copyright© 2021 John Sennesael
Copyright© 2022 John Sennesael
This file is part of justify.
Justify is free software: you can redistribute it and/or modify it under the
@ -25,25 +25,24 @@
#include <string>
#include <vector>
bool justifyCenter(
std::vector<std::wstring> justifyCenter(
std::shared_ptr<std::wistream> in,
std::shared_ptr<std::wostream> out,
size_t width)
{
std::vector<std::wstring> result;
std::vector<std::wstring> parsedLines = truncateLinesToFitWidth(
in,
out,
width
);
for (auto& line: parsedLines)
{
float numberOfSpacesNeeded = (width - line.size()) / 2.0f;
int numberOfSpacesNeeded = (width - line.size()) / 2.0f;
while (numberOfSpacesNeeded > 0)
{
line = L" " + line;
--numberOfSpacesNeeded;
}
*out << line << std::endl;
result.emplace_back(line);
}
return true;
return result;
}

View File

@ -1,5 +1,5 @@
/*
Copyright© 2021 John Sennesael
Copyright© 2022 John Sennesael
This file is part of justify.
Justify is free software: you can redistribute it and/or modify it under the
@ -20,9 +20,10 @@
#include <fstream>
#include <memory>
#include <string>
#include <vector>
bool justifyCenter(
std::vector<std::wstring> justifyCenter(
std::shared_ptr<std::wistream> in,
std::shared_ptr<std::wostream> out,
size_t width);

View File

@ -1,5 +1,5 @@
/*
Copyright© 2021 John Sennesael
Copyright© 2022 John Sennesael
This file is part of justify.
Justify is free software: you can redistribute it and/or modify it under the
@ -22,12 +22,12 @@
#include <fstream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
std::vector<std::wstring> truncateLinesToFitWidth(
std::shared_ptr<std::wistream> in,
std::shared_ptr<std::wostream> out,
size_t width)
{
@ -40,7 +40,10 @@ std::vector<std::wstring> truncateLinesToFitWidth(
{
if (!bufferDone)
{
if (!std::getline(*in, line)) bufferDone = true;
if (!std::getline(*in, line))
{
bufferDone = true;
}
}
else
{
@ -50,23 +53,37 @@ std::vector<std::wstring> truncateLinesToFitWidth(
if (overflow.empty() && bufferDone) break;
line = StringTrim(line);
line = StringRemoveDuplicateWhitespace(line);
if (line.empty() && !bufferDone)
{
parsedLines.emplace_back(L"");
if (overflow.empty()) continue;
}
if (!overflow.empty())
{
line = overflow + L' ' + line;
}
overflow.clear();
if (line.empty())
{
parsedLines.emplace_back(L"");
continue;
}
auto lastWordPos = line.find_last_of(' ', width);
if ((lastWordPos != std::wstring::npos)
&& (line.size() > width))
if (line.size() > width)
{
std::wstring outLine = line.substr(0, lastWordPos);
overflow = line.substr(lastWordPos, line.length());
parsedLines.emplace_back(outLine);
if (lastWordPos != std::wstring::npos)
{
std::wstring outLine = line.substr(0, lastWordPos);
overflow = line.substr(lastWordPos, line.length());
parsedLines.emplace_back(outLine);
}
else
{
// We have a long word that we couldn't fit.
// there's really no good options here... either we
// truncate it, things still look pretty, but we lost content.
// Or we mess up all formatting behind this.
// So instead, let's just throw an error.
throw std::runtime_error(
"Encountered a word that's longer than the supplied width."
" Please provide a longer width so it fits."
);
}
}
else
{

View File

@ -1,5 +1,5 @@
/*
Copyright© 2021 John Sennesael
Copyright© 2022 John Sennesael
This file is part of justify.
Justify is free software: you can redistribute it and/or modify it under the
@ -25,5 +25,4 @@
std::vector<std::wstring> truncateLinesToFitWidth(
std::shared_ptr<std::wistream> in,
std::shared_ptr<std::wostream> out,
size_t width);

View File

@ -1,5 +1,5 @@
/*
Copyright© 2021 John Sennesael
Copyright© 2022 John Sennesael
This file is part of justify.
Justify is free software: you can redistribute it and/or modify it under the
@ -162,15 +162,14 @@ std::vector<std::wstring> injectSpace(
return result;
}
bool justifyFill(
std::vector<std::wstring> justifyFill(
std::shared_ptr<std::wistream> in,
std::shared_ptr<std::wostream> out,
size_t width)
{
std::vector<std::wstring> result;
std::vector<std::wstring> parsedLines = truncateLinesToFitWidth(
in,
out,
width
);
@ -187,14 +186,14 @@ bool justifyFill(
const size_t spacesNeeded = width - line.length();
if (spacesNeeded == 0)
{
*out << line << std::endl;
result.emplace_back(line);
continue;
}
auto vWords = words(line);
const size_t nWords = vWords.size();
if (nWords < 1)
{
*out << line << std::endl;
result.emplace_back(line);
continue;
}
const size_t spacesPerWord = (spacesNeeded + nWords - 1) / nWords;
@ -202,14 +201,14 @@ bool justifyFill(
// avoid injecting an unreasonable amount of whitespace.
if (spacesPerWord > nWords)
{
*out << line << std::endl;
result.emplace_back(line);
continue;
}
// we need at least 2 words for this to work.
if (nWords < 2)
{
*out << line << std::endl;
result.emplace_back(line);
continue;
}
@ -217,7 +216,7 @@ bool justifyFill(
// long, don't justify.
if (nextLine.empty() && (line.size() <= width))
{
*out << line << std::endl;
result.emplace_back(line);
continue;
}
@ -239,7 +238,7 @@ bool justifyFill(
break;
}
}
*out << line << std::endl;
result.emplace_back(line);
}
return true;
return result;
}

View File

@ -1,5 +1,5 @@
/*
Copyright© 2021 John Sennesael
Copyright© 2022 John Sennesael
This file is part of justify.
Justify is free software: you can redistribute it and/or modify it under the
@ -30,7 +30,6 @@ struct WordScore
float score{};
};
bool justifyFill(
std::vector<std::wstring> justifyFill(
std::shared_ptr<std::wistream> in,
std::shared_ptr<std::wostream> out,
size_t width);

View File

@ -1,5 +1,5 @@
/*
Copyright© 2021 John Sennesael
Copyright© 2022 John Sennesael
This file is part of justify.
Justify is free software: you can redistribute it and/or modify it under the
@ -25,21 +25,20 @@
#include <string>
#include <vector>
bool justifyLeft(
std::vector<std::wstring> justifyLeft(
std::shared_ptr<std::wistream> in,
std::shared_ptr<std::wostream> out,
size_t width)
{
std::vector<std::wstring> result;
const std::vector<std::wstring> parsedLines = truncateLinesToFitWidth(
in,
out,
width
);
for (auto& line: parsedLines)
{
*out << line << std::endl;
result.emplace_back(line);
}
return true;
return result;
}

View File

@ -1,5 +1,5 @@
/*
Copyright© 2021 John Sennesael
Copyright© 2022 John Sennesael
This file is part of justify.
Justify is free software: you can redistribute it and/or modify it under the
@ -23,7 +23,6 @@
#include <string>
#include <vector>
bool justifyLeft(
std::vector<std::wstring> justifyLeft(
std::shared_ptr<std::wistream> in,
std::shared_ptr<std::wostream> out,
size_t width);

View File

@ -1,5 +1,5 @@
/*
Copyright© 2021 John Sennesael
Copyright© 2022 John Sennesael
This file is part of justify.
Justify is free software: you can redistribute it and/or modify it under the
@ -25,26 +25,25 @@
#include <string>
#include <vector>
bool justifyRight(
std::vector<std::wstring> justifyRight(
std::shared_ptr<std::wistream> in,
std::shared_ptr<std::wostream> out,
size_t width)
{
std::vector<std::wstring> result;
std::vector<std::wstring> parsedLines = truncateLinesToFitWidth(
in,
out,
width
);
for (auto& line: parsedLines)
{
size_t numberOfSpacesNeeded = width - line.size();
int numberOfSpacesNeeded = width - line.size();
while (numberOfSpacesNeeded > 0)
{
line = L" " + line;
--numberOfSpacesNeeded;
}
*out << line << std::endl;
result.emplace_back(line);
}
return true;
return result;
}

View File

@ -1,5 +1,5 @@
/*
Copyright© 2021 John Sennesael
Copyright© 2022 John Sennesael
This file is part of justify.
Justify is free software: you can redistribute it and/or modify it under the
@ -23,7 +23,6 @@
#include <string>
#include <vector>
bool justifyRight(
std::vector<std::wstring> justifyRight(
std::shared_ptr<std::wistream> in,
std::shared_ptr<std::wostream> out,
size_t width);

View File

@ -1,5 +1,5 @@
/*
Copyright© 2021 John Sennesael
Copyright© 2022 John Sennesael
This file is part of justify.
Justify is free software: you can redistribute it and/or modify it under the