mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 06:47:35 +00:00
Spreadsheet: Port XSV writer to Core::Stream
This commit is contained in:
parent
5793f7749c
commit
3a8450ae11
5 changed files with 74 additions and 120 deletions
|
@ -10,10 +10,9 @@
|
||||||
#include <AK/DeprecatedString.h>
|
#include <AK/DeprecatedString.h>
|
||||||
#include <AK/JsonArray.h>
|
#include <AK/JsonArray.h>
|
||||||
#include <AK/LexicalPath.h>
|
#include <AK/LexicalPath.h>
|
||||||
#include <AK/MemoryStream.h>
|
|
||||||
#include <Applications/Spreadsheet/CSVExportGML.h>
|
#include <Applications/Spreadsheet/CSVExportGML.h>
|
||||||
#include <LibCore/File.h>
|
|
||||||
#include <LibCore/FileStream.h>
|
#include <LibCore/FileStream.h>
|
||||||
|
#include <LibCore/MemoryStream.h>
|
||||||
#include <LibCore/StandardPaths.h>
|
#include <LibCore/StandardPaths.h>
|
||||||
#include <LibGUI/Application.h>
|
#include <LibGUI/Application.h>
|
||||||
#include <LibGUI/CheckBox.h>
|
#include <LibGUI/CheckBox.h>
|
||||||
|
@ -90,7 +89,7 @@ CSVExportDialogPage::CSVExportDialogPage(Sheet const& sheet)
|
||||||
update_preview();
|
update_preview();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto CSVExportDialogPage::make_writer(OutputStream& stream) -> ErrorOr<XSV>
|
auto CSVExportDialogPage::make_writer(Core::Stream::Handle<Core::Stream::Stream> stream) -> ErrorOr<NonnullOwnPtr<XSV>>
|
||||||
{
|
{
|
||||||
auto delimiter = TRY([this]() -> ErrorOr<DeprecatedString> {
|
auto delimiter = TRY([this]() -> ErrorOr<DeprecatedString> {
|
||||||
if (m_delimiter_other_radio->is_checked()) {
|
if (m_delimiter_other_radio->is_checked()) {
|
||||||
|
@ -152,23 +151,22 @@ auto CSVExportDialogPage::make_writer(OutputStream& stream) -> ErrorOr<XSV>
|
||||||
if (should_quote_all_fields)
|
if (should_quote_all_fields)
|
||||||
behaviors = behaviors | Writer::WriterBehavior::QuoteAll;
|
behaviors = behaviors | Writer::WriterBehavior::QuoteAll;
|
||||||
|
|
||||||
return XSV(stream, m_data, move(traits), *headers, behaviors);
|
return try_make<XSV>(move(stream), m_data, move(traits), *headers, behaviors);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSVExportDialogPage::update_preview()
|
void CSVExportDialogPage::update_preview()
|
||||||
{
|
{
|
||||||
DuplexMemoryStream memory_stream;
|
auto maybe_error = [this]() -> ErrorOr<void> {
|
||||||
auto writer_or_error = make_writer(memory_stream);
|
Core::Stream::AllocatingMemoryStream memory_stream;
|
||||||
if (!writer_or_error.is_error()) {
|
auto writer = TRY(make_writer(Core::Stream::Handle<Core::Stream::Stream>(memory_stream)));
|
||||||
m_data_preview_text_editor->set_text(DeprecatedString::formatted("Cannot update preview: {}", writer_or_error.error()));
|
TRY(writer->generate_preview());
|
||||||
return;
|
auto buffer = TRY(memory_stream.read_until_eof());
|
||||||
}
|
m_data_preview_text_editor->set_text(StringView(buffer));
|
||||||
auto writer = writer_or_error.release_value();
|
m_data_preview_text_editor->update();
|
||||||
|
return {};
|
||||||
writer.generate_preview();
|
}();
|
||||||
auto buffer = memory_stream.copy_into_contiguous_buffer();
|
if (maybe_error.is_error())
|
||||||
m_data_preview_text_editor->set_text(StringView(buffer));
|
m_data_preview_text_editor->set_text(DeprecatedString::formatted("Cannot update preview: {}", maybe_error.error()));
|
||||||
m_data_preview_text_editor->update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorOr<void> ExportDialog::make_and_run_for(StringView mime, Core::File& file, Workbook& workbook)
|
ErrorOr<void> ExportDialog::make_and_run_for(StringView mime, Core::File& file, Workbook& workbook)
|
||||||
|
@ -188,12 +186,9 @@ ErrorOr<void> ExportDialog::make_and_run_for(StringView mime, Core::File& file,
|
||||||
if (wizard->exec() != GUI::Dialog::ExecResult::OK)
|
if (wizard->exec() != GUI::Dialog::ExecResult::OK)
|
||||||
return Error::from_string_literal("CSV Export was cancelled");
|
return Error::from_string_literal("CSV Export was cancelled");
|
||||||
|
|
||||||
auto file_stream = Core::OutputFileStream(file);
|
auto file_stream = TRY(try_make<Core::Stream::WrappedAKOutputStream>(TRY(try_make<Core::OutputFileStream>(file))));
|
||||||
auto writer = TRY(page.make_writer(file_stream));
|
auto writer = TRY(page.make_writer(move(file_stream)));
|
||||||
writer.generate();
|
return writer->generate();
|
||||||
if (writer.has_error())
|
|
||||||
return Error::from_string_literal("CSV Export failed");
|
|
||||||
return {};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
auto export_worksheet = [&]() -> ErrorOr<void> {
|
auto export_worksheet = [&]() -> ErrorOr<void> {
|
||||||
|
|
|
@ -23,7 +23,7 @@ struct CSVExportDialogPage {
|
||||||
explicit CSVExportDialogPage(Sheet const&);
|
explicit CSVExportDialogPage(Sheet const&);
|
||||||
|
|
||||||
NonnullRefPtr<GUI::WizardPage> page() { return *m_page; }
|
NonnullRefPtr<GUI::WizardPage> page() { return *m_page; }
|
||||||
ErrorOr<XSV> make_writer(OutputStream&);
|
ErrorOr<NonnullOwnPtr<XSV>> make_writer(Core::Stream::Handle<Core::Stream::Stream>);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void update_preview();
|
void update_preview();
|
||||||
|
|
|
@ -15,8 +15,8 @@ namespace Writer {
|
||||||
template<typename ContainerType>
|
template<typename ContainerType>
|
||||||
class CSV : public XSV<ContainerType> {
|
class CSV : public XSV<ContainerType> {
|
||||||
public:
|
public:
|
||||||
CSV(OutputStream& output, ContainerType const& data, Vector<StringView> const& headers = {}, WriterBehavior behaviors = default_behaviors())
|
CSV(Core::Stream::Handle<Core::Stream::Stream> output, ContainerType const& data, Vector<StringView> headers = {}, WriterBehavior behaviors = default_behaviors())
|
||||||
: XSV<ContainerType>(output, data, { ",", "\"", WriterTraits::Repeat }, headers, behaviors)
|
: XSV<ContainerType>(move(output), data, { ",", "\"", WriterTraits::Repeat }, move(headers), behaviors)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
#include "../CSV.h"
|
#include "../CSV.h"
|
||||||
#include "../XSV.h"
|
#include "../XSV.h"
|
||||||
#include <AK/MemoryStream.h>
|
#include <LibCore/MemoryStream.h>
|
||||||
|
|
||||||
TEST_CASE(can_write)
|
TEST_CASE(can_write)
|
||||||
{
|
{
|
||||||
|
@ -18,17 +18,17 @@ TEST_CASE(can_write)
|
||||||
{ 7, 8, 9 },
|
{ 7, 8, 9 },
|
||||||
};
|
};
|
||||||
|
|
||||||
auto buffer = ByteBuffer::create_uninitialized(1024).release_value();
|
Core::Stream::AllocatingMemoryStream stream;
|
||||||
OutputMemoryStream stream { buffer };
|
auto csv = Writer::CSV(Core::Stream::Handle<Core::Stream::Stream>(stream), data);
|
||||||
|
MUST(csv.generate());
|
||||||
Writer::CSV csv(stream, data);
|
|
||||||
|
|
||||||
auto expected_output = R"~(1,2,3
|
auto expected_output = R"~(1,2,3
|
||||||
4,5,6
|
4,5,6
|
||||||
7,8,9
|
7,8,9
|
||||||
)~";
|
)~"sv;
|
||||||
|
|
||||||
EXPECT_EQ(StringView { stream.bytes() }, expected_output);
|
auto buffer = MUST(stream.read_until_eof());
|
||||||
|
EXPECT_EQ(StringView { buffer.bytes() }, expected_output);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE(can_write_with_header)
|
TEST_CASE(can_write_with_header)
|
||||||
|
@ -39,18 +39,18 @@ TEST_CASE(can_write_with_header)
|
||||||
{ 7, 8, 9 },
|
{ 7, 8, 9 },
|
||||||
};
|
};
|
||||||
|
|
||||||
auto buffer = ByteBuffer::create_uninitialized(1024).release_value();
|
Core::Stream::AllocatingMemoryStream stream;
|
||||||
OutputMemoryStream stream { buffer };
|
auto csv = Writer::CSV(Core::Stream::Handle<Core::Stream::Stream>(stream), data, { "A"sv, "B\""sv, "C"sv });
|
||||||
|
MUST(csv.generate());
|
||||||
Writer::CSV csv(stream, data, { "A"sv, "B\""sv, "C"sv });
|
|
||||||
|
|
||||||
auto expected_output = R"~(A,"B""",C
|
auto expected_output = R"~(A,"B""",C
|
||||||
1,2,3
|
1,2,3
|
||||||
4,5,6
|
4,5,6
|
||||||
7,8,9
|
7,8,9
|
||||||
)~";
|
)~"sv;
|
||||||
|
|
||||||
EXPECT_EQ(StringView { stream.bytes() }, expected_output);
|
auto buffer = MUST(stream.read_until_eof());
|
||||||
|
EXPECT_EQ(StringView { buffer.bytes() }, expected_output);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE(can_write_with_different_behaviors)
|
TEST_CASE(can_write_with_different_behaviors)
|
||||||
|
@ -60,15 +60,15 @@ TEST_CASE(can_write_with_different_behaviors)
|
||||||
{ "We\"ll", "Hello,", " Friends" },
|
{ "We\"ll", "Hello,", " Friends" },
|
||||||
};
|
};
|
||||||
|
|
||||||
auto buffer = ByteBuffer::create_uninitialized(1024).release_value();
|
Core::Stream::AllocatingMemoryStream stream;
|
||||||
OutputMemoryStream stream { buffer };
|
auto csv = Writer::CSV(Core::Stream::Handle<Core::Stream::Stream>(stream), data, { "A"sv, "B\""sv, "C"sv }, Writer::WriterBehavior::QuoteOnlyInFieldStart | Writer::WriterBehavior::WriteHeaders);
|
||||||
|
MUST(csv.generate());
|
||||||
Writer::CSV csv(stream, data, { "A"sv, "B\""sv, "C"sv }, Writer::WriterBehavior::QuoteOnlyInFieldStart | Writer::WriterBehavior::WriteHeaders);
|
|
||||||
|
|
||||||
auto expected_output = R"~(A,B",C
|
auto expected_output = R"~(A,B",C
|
||||||
Well,Hello",Friends
|
Well,Hello",Friends
|
||||||
We"ll,"Hello,", Friends
|
We"ll,"Hello,", Friends
|
||||||
)~";
|
)~"sv;
|
||||||
|
|
||||||
EXPECT_EQ(StringView { stream.bytes() }, expected_output);
|
auto buffer = MUST(stream.read_until_eof());
|
||||||
|
EXPECT_EQ(StringView { buffer.bytes() }, expected_output);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,10 @@
|
||||||
#include <AK/DeprecatedString.h>
|
#include <AK/DeprecatedString.h>
|
||||||
#include <AK/GenericLexer.h>
|
#include <AK/GenericLexer.h>
|
||||||
#include <AK/OwnPtr.h>
|
#include <AK/OwnPtr.h>
|
||||||
#include <AK/Stream.h>
|
|
||||||
#include <AK/StringView.h>
|
#include <AK/StringView.h>
|
||||||
#include <AK/Types.h>
|
#include <AK/Types.h>
|
||||||
#include <AK/Vector.h>
|
#include <AK/Vector.h>
|
||||||
|
#include <LibCore/Stream.h>
|
||||||
|
|
||||||
namespace Writer {
|
namespace Writer {
|
||||||
|
|
||||||
|
@ -34,17 +34,6 @@ struct WriterTraits {
|
||||||
} quote_escape { Repeat };
|
} quote_escape { Repeat };
|
||||||
};
|
};
|
||||||
|
|
||||||
#define ENUMERATE_WRITE_ERRORS() \
|
|
||||||
E(None, "No errors") \
|
|
||||||
E(NonConformingColumnCount, "Header count does not match given column count") \
|
|
||||||
E(InternalError, "Internal error")
|
|
||||||
|
|
||||||
enum class WriteError {
|
|
||||||
#define E(name, _) name,
|
|
||||||
ENUMERATE_WRITE_ERRORS()
|
|
||||||
#undef E
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr WriterBehavior default_behaviors()
|
constexpr WriterBehavior default_behaviors()
|
||||||
{
|
{
|
||||||
return WriterBehavior::None;
|
return WriterBehavior::None;
|
||||||
|
@ -53,12 +42,12 @@ constexpr WriterBehavior default_behaviors()
|
||||||
template<typename ContainerType, typename HeaderType = Vector<StringView>>
|
template<typename ContainerType, typename HeaderType = Vector<StringView>>
|
||||||
class XSV {
|
class XSV {
|
||||||
public:
|
public:
|
||||||
XSV(OutputStream& output, ContainerType const& data, WriterTraits traits, HeaderType const& headers = {}, WriterBehavior behaviors = default_behaviors())
|
XSV(Core::Stream::Handle<Core::Stream::Stream> output, ContainerType const& data, WriterTraits traits, HeaderType headers = {}, WriterBehavior behaviors = default_behaviors())
|
||||||
: m_data(data)
|
: m_data(data)
|
||||||
, m_traits(move(traits))
|
, m_traits(move(traits))
|
||||||
, m_behaviors(behaviors)
|
, m_behaviors(behaviors)
|
||||||
, m_names(headers)
|
, m_names(headers)
|
||||||
, m_output(output)
|
, m_output(move(output))
|
||||||
{
|
{
|
||||||
if (!headers.is_empty())
|
if (!headers.is_empty())
|
||||||
m_behaviors = m_behaviors | WriterBehavior::WriteHeaders;
|
m_behaviors = m_behaviors | WriterBehavior::WriteHeaders;
|
||||||
|
@ -66,94 +55,71 @@ public:
|
||||||
|
|
||||||
virtual ~XSV() = default;
|
virtual ~XSV() = default;
|
||||||
|
|
||||||
bool has_error() const { return m_error != WriteError::None; }
|
ErrorOr<void> generate()
|
||||||
WriteError error() const { return m_error; }
|
|
||||||
DeprecatedString error_string() const
|
|
||||||
{
|
|
||||||
switch (m_error) {
|
|
||||||
#define E(x, y) \
|
|
||||||
case WriteError::x: \
|
|
||||||
return y;
|
|
||||||
|
|
||||||
ENUMERATE_WRITE_ERRORS();
|
|
||||||
#undef E
|
|
||||||
}
|
|
||||||
VERIFY_NOT_REACHED();
|
|
||||||
}
|
|
||||||
|
|
||||||
void generate()
|
|
||||||
{
|
{
|
||||||
auto with_headers = has_flag(m_behaviors, WriterBehavior::WriteHeaders);
|
auto with_headers = has_flag(m_behaviors, WriterBehavior::WriteHeaders);
|
||||||
if (with_headers) {
|
if (with_headers) {
|
||||||
write_row(m_names);
|
TRY(write_row(m_names));
|
||||||
if (m_output.write({ "\n", 1 }) != 1)
|
TRY(m_output->write_entire_buffer({ "\n", 1 }));
|
||||||
set_error(WriteError::InternalError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto&& row : m_data) {
|
for (auto&& row : m_data) {
|
||||||
if (with_headers) {
|
if (with_headers) {
|
||||||
if (row.size() != m_names.size())
|
if (row.size() != m_names.size())
|
||||||
set_error(WriteError::NonConformingColumnCount);
|
return Error::from_string_literal("Header count does not match given column count");
|
||||||
}
|
}
|
||||||
|
|
||||||
write_row(row);
|
TRY(write_row(row));
|
||||||
if (m_output.write({ "\n", 1 }) != 1)
|
TRY(m_output->write_entire_buffer({ "\n", 1 }));
|
||||||
set_error(WriteError::InternalError);
|
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
void generate_preview()
|
ErrorOr<void> generate_preview()
|
||||||
{
|
{
|
||||||
auto lines_written = 0;
|
auto lines_written = 0;
|
||||||
constexpr auto max_preview_lines = 8;
|
constexpr auto max_preview_lines = 8;
|
||||||
|
|
||||||
auto with_headers = has_flag(m_behaviors, WriterBehavior::WriteHeaders);
|
auto with_headers = has_flag(m_behaviors, WriterBehavior::WriteHeaders);
|
||||||
if (with_headers) {
|
if (with_headers) {
|
||||||
write_row(m_names);
|
TRY(write_row(m_names));
|
||||||
if (m_output.write({ "\n", 1 }) != 1)
|
TRY(m_output->write_entire_buffer({ "\n", 1 }));
|
||||||
set_error(WriteError::InternalError);
|
|
||||||
++lines_written;
|
++lines_written;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto&& row : m_data) {
|
for (auto&& row : m_data) {
|
||||||
if (with_headers) {
|
if (with_headers) {
|
||||||
if (row.size() != m_names.size())
|
if (row.size() != m_names.size())
|
||||||
set_error(WriteError::NonConformingColumnCount);
|
return Error::from_string_literal("Header count does not match given column count");
|
||||||
}
|
}
|
||||||
|
|
||||||
write_row(row);
|
TRY(write_row(row));
|
||||||
if (m_output.write({ "\n", 1 }) != 1)
|
TRY(m_output->write_entire_buffer({ "\n", 1 }));
|
||||||
set_error(WriteError::InternalError);
|
|
||||||
++lines_written;
|
++lines_written;
|
||||||
|
|
||||||
if (lines_written >= max_preview_lines)
|
if (lines_written >= max_preview_lines)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void set_error(WriteError error)
|
|
||||||
{
|
|
||||||
if (m_error == WriteError::None)
|
|
||||||
m_error = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
void write_row(T&& row)
|
ErrorOr<void> write_row(T&& row)
|
||||||
{
|
{
|
||||||
bool first = true;
|
bool first = true;
|
||||||
for (auto&& entry : row) {
|
for (auto&& entry : row) {
|
||||||
if (!first) {
|
if (!first) {
|
||||||
if (m_output.write(m_traits.separator.bytes()) != m_traits.separator.length())
|
TRY(m_output->write_entire_buffer(m_traits.separator.bytes()));
|
||||||
set_error(WriteError::InternalError);
|
|
||||||
}
|
}
|
||||||
first = false;
|
first = false;
|
||||||
write_entry(entry);
|
TRY(write_entry(entry));
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
void write_entry(T&& entry)
|
ErrorOr<void> write_entry(T&& entry)
|
||||||
{
|
{
|
||||||
auto string = DeprecatedString::formatted("{}", FormatIfSupported(entry));
|
auto string = DeprecatedString::formatted("{}", FormatIfSupported(entry));
|
||||||
|
|
||||||
|
@ -169,49 +135,42 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
if (safe_to_write_normally) {
|
if (safe_to_write_normally) {
|
||||||
if (m_output.write(string.bytes()) != string.length())
|
if (!string.is_empty())
|
||||||
set_error(WriteError::InternalError);
|
TRY(m_output->write_entire_buffer(string.bytes()));
|
||||||
return;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
|
TRY(m_output->write_entire_buffer(m_traits.quote.bytes()));
|
||||||
set_error(WriteError::InternalError);
|
|
||||||
|
|
||||||
GenericLexer lexer(string);
|
GenericLexer lexer(string);
|
||||||
while (!lexer.is_eof()) {
|
while (!lexer.is_eof()) {
|
||||||
if (lexer.consume_specific(m_traits.quote)) {
|
if (lexer.consume_specific(m_traits.quote)) {
|
||||||
switch (m_traits.quote_escape) {
|
switch (m_traits.quote_escape) {
|
||||||
case WriterTraits::Repeat:
|
case WriterTraits::Repeat:
|
||||||
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
|
TRY(m_output->write_entire_buffer(m_traits.quote.bytes()));
|
||||||
set_error(WriteError::InternalError);
|
TRY(m_output->write_entire_buffer(m_traits.quote.bytes()));
|
||||||
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
|
|
||||||
set_error(WriteError::InternalError);
|
|
||||||
break;
|
break;
|
||||||
case WriterTraits::Backslash:
|
case WriterTraits::Backslash:
|
||||||
if (m_output.write({ "\\", 1 }) != 1)
|
TRY(m_output->write_entire_buffer({ "\\", 1 }));
|
||||||
set_error(WriteError::InternalError);
|
TRY(m_output->write_entire_buffer(m_traits.quote.bytes()));
|
||||||
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
|
|
||||||
set_error(WriteError::InternalError);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ch = lexer.consume();
|
auto ch = lexer.consume();
|
||||||
if (m_output.write({ &ch, 1 }) != 1)
|
TRY(m_output->write_entire_buffer({ &ch, 1 }));
|
||||||
set_error(WriteError::InternalError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
|
TRY(m_output->write_entire_buffer(m_traits.quote.bytes()));
|
||||||
set_error(WriteError::InternalError);
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
ContainerType const& m_data;
|
ContainerType const& m_data;
|
||||||
WriterTraits m_traits;
|
WriterTraits m_traits;
|
||||||
WriterBehavior m_behaviors;
|
WriterBehavior m_behaviors;
|
||||||
HeaderType const& m_names;
|
HeaderType m_names;
|
||||||
WriteError m_error { WriteError::None };
|
Core::Stream::Handle<Core::Stream::Stream> m_output;
|
||||||
OutputStream& m_output;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue