tdf#144576 Copy a table from Writer to plain text editor as a Matrix

sw ascii filter
 - check for table nodes, output them seperately with formating
   to be displayed as a matrix when copy/pasted to a text file

sw qa filter ascii
 - add new test suite along with test to check for correct output

Change-Id: I8ca31bced3860e8e9752db8530ea6caaf31f2e5e
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/164833
Reviewed-by: Hossein <hossein@libreoffice.org>
Tested-by: Jenkins
diff --git a/sw/CppunitTest_sw_filter_ascii.mk b/sw/CppunitTest_sw_filter_ascii.mk
new file mode 100644
index 0000000..85a648c
--- /dev/null
+++ b/sw/CppunitTest_sw_filter_ascii.mk
@@ -0,0 +1,76 @@
# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*-
#*************************************************************************
#
# This file is part of the LibreOffice project.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
#*************************************************************************

$(eval $(call gb_CppunitTest_CppunitTest,sw_filter_ascii))

$(eval $(call gb_CppunitTest_use_common_precompiled_header,sw_filter_ascii))

$(eval $(call gb_CppunitTest_add_exception_objects,sw_filter_ascii, \
    sw/qa/filter/ascii/ascii \
))

$(eval $(call gb_CppunitTest_use_libraries,sw_filter_ascii, \
    comphelper \
    cppu \
    cppuhelper \
    editeng \
    sal \
    sfx \
    subsequenttest \
    svl \
    svx \
    svxcore \
    sw \
    swqahelper \
    test \
    unotest \
    utl \
    vcl \
    tl \
))

$(eval $(call gb_CppunitTest_use_externals,sw_filter_ascii,\
    boost_headers \
    libxml2 \
))

$(eval $(call gb_CppunitTest_set_include,sw_filter_ascii,\
    -I$(SRCDIR)/sw/inc \
    -I$(SRCDIR)/sw/source/core/inc \
    -I$(SRCDIR)/sw/source/uibase/inc \
    -I$(SRCDIR)/sw/qa/inc \
    $$(INCLUDE) \
))

$(eval $(call gb_CppunitTest_use_api,sw_filter_ascii,\
	udkapi \
	offapi \
	oovbaapi \
))

$(eval $(call gb_CppunitTest_use_ure,sw_filter_ascii))
$(eval $(call gb_CppunitTest_use_vcl,sw_filter_ascii))

$(eval $(call gb_CppunitTest_use_rdb,sw_filter_ascii,services))

$(eval $(call gb_CppunitTest_use_custom_headers,sw_filter_ascii,\
    officecfg/registry \
))

$(eval $(call gb_CppunitTest_use_configuration,sw_filter_ascii))

$(eval $(call gb_CppunitTest_use_uiconfigs,sw_filter_ascii, \
    modules/swriter \
))

$(eval $(call gb_CppunitTest_use_more_fonts,sw_filter_ascii))

# vim: set noet sw=4 ts=4:
diff --git a/sw/Module_sw.mk b/sw/Module_sw.mk
index fc2afe9..2db6fba 100644
--- a/sw/Module_sw.mk
+++ b/sw/Module_sw.mk
@@ -167,6 +167,7 @@ $(eval $(call gb_Module_add_slowcheck_targets,sw,\
    CppunitTest_sw_filter_ww8 \
    CppunitTest_sw_filter_html \
    CppunitTest_sw_filter_xml \
    CppunitTest_sw_filter_ascii \
    CppunitTest_sw_a11y \
    CppunitTest_sw_core_theme \
    CppunitTest_sw_pdf_test \
diff --git a/sw/qa/filter/ascii/ascii.cxx b/sw/qa/filter/ascii/ascii.cxx
new file mode 100644
index 0000000..d6c4773
--- /dev/null
+++ b/sw/qa/filter/ascii/ascii.cxx
@@ -0,0 +1,153 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <swmodeltestbase.hxx>

#include <unotxdoc.hxx>
#include <itabenum.hxx>
#include <wrtsh.hxx>
#include <com/sun/star/text/XTextTable.hpp>
#include <ndtxt.hxx>
#include <swdtflvr.hxx>
#include <swmodule.hxx>

namespace
{
/**
 * Covers sw/source/filter/ascii/ fixes.
 *
 * Note that these tests are meant to be simple: either load a file and assert some result or build
 * a document model with code, export and assert that result.
 *
 * Keep using the various sw_<format>import/export suites for multiple filter calls inside a single
 * test.
 */
class Test : public SwModelTestBase
{
public:
    Test()
        : SwModelTestBase("/sw/qa/filter/ascii/data/")
    {
    }
};

CPPUNIT_TEST_FIXTURE(Test, testTdf144576_ascii)
{
    // Given a document with a 2x2 table
    createSwDoc();
    SwDoc* pDoc = getSwDoc();
    SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell();
    SwInsertTableOptions aTableOptions(SwInsertTableFlags::DefaultBorder, 0);
    pWrtShell->InsertTable(aTableOptions, /*nRows=*/2, /*nCols=*/2);

    uno::Reference<text::XTextTablesSupplier> xTablesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<container::XNameAccess> xTableNames = xTablesSupplier->getTextTables();
    CPPUNIT_ASSERT(xTableNames->hasByName("Table1"));
    uno::Reference<text::XTextTable> xTable1(xTableNames->getByName("Table1"), uno::UNO_QUERY);

    pWrtShell->GotoTable(u"Table1"_ustr);
    pWrtShell->Insert(u"A"_ustr);
    pWrtShell->GoNextCell(false);
    pWrtShell->Insert(u"B"_ustr);
    pWrtShell->GoNextCell(false);
    pWrtShell->Insert(u"C"_ustr);
    pWrtShell->GoNextCell(false);
    pWrtShell->Insert(u"D"_ustr);

    // Select the whole table
    dispatchCommand(mxComponent, ".uno:SelectAll", {});
    dispatchCommand(mxComponent, ".uno:SelectAll", {});
    dispatchCommand(mxComponent, ".uno:SelectAll", {});

    rtl::Reference<SwTransferable> xTransferable(new SwTransferable(*pWrtShell));
    xTransferable->Copy(); // Ctl-C
    xTransferable.get();

    // Get the plain text version of the selection
    datatransfer::DataFlavor aFlavor;
    aFlavor.MimeType = "text/plain;charset=utf-16";
    aFlavor.DataType = cppu::UnoType<OUString>::get();
    uno::Any aData = xTransferable->getTransferData(aFlavor);
    OUString aActual;
    aData >>= aActual;
    pWrtShell->ClearMark();

    CPPUNIT_ASSERT(aData.hasValue());

    OUString aExpected = u"A\tB"_ustr SAL_NEWLINE_STRING u"C\tD"_ustr SAL_NEWLINE_STRING u""_ustr;

    // Without the fix in place, the test fails with:
    // - Expected: A    B
    //             C    D
    //
    // - Actual  : A
    //             B
    //             C
    //             D
    // i.e. Each cell is seperated by a tab
    CPPUNIT_ASSERT_EQUAL(aExpected, aActual);

    // Add some newlines in the first two cells
    // and test to see if they're handled correctly
    uno::Reference<text::XTextRange> xCellA1(xTable1->getCellByName("A1"), uno::UNO_QUERY);
    xCellA1->setString(u""_ustr);
    uno::Reference<text::XTextRange> xCellB1(xTable1->getCellByName("B1"), uno::UNO_QUERY);
    xCellB1->setString(u""_ustr);

    pWrtShell->GotoTable(u"Table1"_ustr);
    pWrtShell->Insert(u"A"_ustr);
    pWrtShell->SplitNode();
    pWrtShell->Insert(u"A1"_ustr);
    pWrtShell->GoNextCell(false);
    pWrtShell->Insert(u"B"_ustr);
    pWrtShell->SplitNode();
    pWrtShell->Insert(u"B1"_ustr);
    pWrtShell->SplitNode();
    pWrtShell->Insert(u"B2"_ustr);

    aExpected
        = u"A"_ustr SAL_NEWLINE_STRING u"A1\tB"_ustr SAL_NEWLINE_STRING u"B1"_ustr SAL_NEWLINE_STRING u"B2"_ustr SAL_NEWLINE_STRING u"C\tD"_ustr SAL_NEWLINE_STRING u""_ustr;

    // Select the whole table
    dispatchCommand(mxComponent, ".uno:SelectAll", {});
    dispatchCommand(mxComponent, ".uno:SelectAll", {});
    dispatchCommand(mxComponent, ".uno:SelectAll", {});

    // Get the plain text version of the selection
    rtl::Reference<SwTransferable> xNewTransferable(new SwTransferable(*pWrtShell));
    xNewTransferable->Copy(); // Ctl-C
    xNewTransferable.get();
    aData = xNewTransferable->getTransferData(aFlavor);
    CPPUNIT_ASSERT(aData.hasValue());
    aData >>= aActual;

    // Without the fix in place, the test fails with:
    // - Expected: A
    //             A1   B
    //             B1
    //             B2
    //             C    D
    //
    // - Actual  : A
    //             A1
    //             B
    //             B1
    //             B2
    //             C
    //             D
    // i.e. Each cell is seperated by a tab, a newline inside
    // a cell gets written to the next line in the furthest
    // left spot available
    CPPUNIT_ASSERT_EQUAL(aExpected, aActual);
}
}

CPPUNIT_PLUGIN_IMPLEMENT();

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/filter/ascii/wrtasc.cxx b/sw/source/filter/ascii/wrtasc.cxx
index 0f1e368..39f5ec9 100644
--- a/sw/source/filter/ascii/wrtasc.cxx
+++ b/sw/source/filter/ascii/wrtasc.cxx
@@ -186,7 +186,14 @@ ErrCode SwASCWriter::WriteStream()
                        }
                        bWriteSttTag = false;
                    }
                    Out( aASCNodeFnTab, *pNd, *this );

                    SwTableNode* pTableNd = pNd->FindTableNode();

                    // Handle a table
                    if (pTableNd && m_bWriteAll)
                        WriteTable(pTableNd, pNd);
                    else
                        Out( aASCNodeFnTab, *pNd, *this );
                }
                bTstFly = false;        // Testing once is enough
            }
@@ -221,6 +228,56 @@ void SwASCWriter::SetupFilterOptions(SfxMedium& rMedium)
    }
}

void SwASCWriter::WriteTable(SwTableNode* pTableNd, SwTextNode* pNd)
{
    OUString sPreLineEnd = this->m_sLineEnd;
    m_sLineEnd = u""_ustr;

    const SwTableLine* pEndTabLine = pTableNd->GetTable().GetTabLines().back();
    const SwTableBox* pEndTabBox = pEndTabLine->GetTabBoxes().back();

    for( const SwTableLine* pLine : pTableNd->GetTable().GetTabLines() )
    {
        for( const SwTableBox* pBox : pLine->GetTabBoxes() )
        {
            Out( aASCNodeFnTab, *pNd, *this );

            Point aPrevBoxPoint = pNd->GetTableBox()->GetCoordinates();
            m_pCurrentPam->Move(fnMoveForward, GoInNode);
            pNd = m_pCurrentPam->GetPoint()->GetNode().GetTextNode();

            // Line break in a box
            // Each line is a new SwTextNode so we
            // need to parse inside the current box
            while (pNd->GetTableBox() && (pNd->GetTableBox()->GetCoordinates() == aPrevBoxPoint))
            {
                Strm().WriteUnicodeOrByteText(sPreLineEnd);
                Out(aASCNodeFnTab, *pNd, *this);

                m_pCurrentPam->Move(fnMoveForward, GoInNode);
                pNd = m_pCurrentPam->GetPoint()->GetNode().GetTextNode();
            }
            if (pBox != pLine->GetTabBoxes().back())
                Strm().WriteUChar( 0x9 );

            if (pBox == pEndTabBox)
                this->m_sLineEnd = sPreLineEnd;

        }// end for each Box

        if (pLine == pEndTabLine)
        {
            m_pCurrentPam->Move(fnMoveBackward, GoInNode);
            pNd = m_pCurrentPam->GetPoint()->GetNode().GetTextNode();
            Strm().WriteUnicodeOrByteText( sPreLineEnd );
        }
        if (pLine != pEndTabLine)
            Strm().WriteUnicodeOrByteText( sPreLineEnd );

    }// end For each row
    this->m_sLineEnd = sPreLineEnd;
}

void GetASCWriter(
    std::u16string_view rFltNm, [[maybe_unused]] const OUString& /*rBaseURL*/, WriterRef& xRet )
{
diff --git a/sw/source/filter/ascii/wrtasc.hxx b/sw/source/filter/ascii/wrtasc.hxx
index c2e3dfa..7464c4e 100644
--- a/sw/source/filter/ascii/wrtasc.hxx
+++ b/sw/source/filter/ascii/wrtasc.hxx
@@ -31,6 +31,7 @@ class SwASCWriter : public Writer
    OUString m_sLineEnd;

    virtual ErrCode WriteStream() override;
    void WriteTable(SwTableNode* pTableNd, SwTextNode* pNd);

public:
    SwASCWriter(std::u16string_view rFilterName);