sw/inc/editsh.hxx | 2 - sw/qa/filter/md/md.cxx | 51 +++++++++++++++++++++++++++++++++++++++++ sw/source/filter/md/wrtmd.cxx | 32 ++++++++++++++++++++++++- sw/source/filter/md/wrtmd.hxx | 6 ++++ sw/source/uibase/inc/wrtsh.hxx | 2 - 5 files changed, 90 insertions(+), 3 deletions(-)
New commits: commit 3b6282b23c4ed8b0ae6229cc65744e0e77f4ebc1 Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Fri Aug 29 09:03:06 2025 +0200 Commit: Caolán McNamara <caolan.mcnam...@collabora.com> CommitDate: Fri Aug 29 11:08:06 2025 +0200 tdf#168152 sw markdown export: handle lists Support the following cases: 1) Toplevel bullet list 2) Nested bullet list (needs indentation) 3) Toplevel numbered list 4) Nested numbered list This is similar to how the ascii export adds an indent, bullets and numbering for text nodes with a numbering rule, but <https://spec.commonmark.org/0.31.2/#list-items> examples 294 -> 297 shows that the markdown indent has to be dynamic, based on the size of the parent prefix, so write dynamic amount of indent, instead of the ascii export's fixed 4 spaces. Change-Id: Ia9eb0c718fc5f5334f19b592245b50bd32e8bd3e Reviewed-on: https://gerrit.libreoffice.org/c/core/+/190353 Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> Reviewed-by: Caolán McNamara <caolan.mcnam...@collabora.com> diff --git a/sw/inc/editsh.hxx b/sw/inc/editsh.hxx index cd79190ba6c7..0e42dca687ee 100644 --- a/sw/inc/editsh.hxx +++ b/sw/inc/editsh.hxx @@ -530,7 +530,7 @@ public: void NoNum(); /// Delete, split enumeration list. - void DelNumRules(); + SW_DLLPUBLIC void DelNumRules(); SW_DLLPUBLIC void NumUpDown( bool bDown = true ); diff --git a/sw/qa/filter/md/md.cxx b/sw/qa/filter/md/md.cxx index ce747675f6b7..50e0dbd048ec 100644 --- a/sw/qa/filter/md/md.cxx +++ b/sw/qa/filter/md/md.cxx @@ -218,6 +218,57 @@ CPPUNIT_TEST_FIXTURE(Test, testExportingCodeSpan) CPPUNIT_ASSERT_EQUAL(aExpected, aActual); } +CPPUNIT_TEST_FIXTURE(Test, testExportingList) +{ + // Given a document that has both toplevel/nested bullets/numberings: + createSwDoc(); + SwDocShell* pDocShell = getSwDocShell(); + SwWrtShell* pWrtShell = pDocShell->GetWrtShell(); + pWrtShell->Insert(u"A"_ustr); + pWrtShell->SplitNode(); + pWrtShell->Insert(u"B"_ustr); + pWrtShell->BulletOn(); + pWrtShell->SplitNode(); + pWrtShell->Insert(u"C"_ustr); + pWrtShell->NumUpDown(/*bDown=*/true); + pWrtShell->SplitNode(); + pWrtShell->Insert(u"D"_ustr); + pWrtShell->DelNumRules(); + pWrtShell->SplitNode(); + pWrtShell->Insert(u"E"_ustr); + pWrtShell->NumOn(); + pWrtShell->SplitNode(); + pWrtShell->Insert(u"F"_ustr); + pWrtShell->SplitNode(); + pWrtShell->Insert(u"G"_ustr); + pWrtShell->NumUpDown(/*bDown=*/true); + + // When saving that to markdown: + save(mpFilter); + + // Then make sure list type and level is exported: + SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ); + std::vector<char> aBuffer(aStream.remainingSize()); + aStream.ReadBytes(aBuffer.data(), aBuffer.size()); + std::string_view aActual(aBuffer.data(), aBuffer.size()); + std::string_view aExpected( + // clang-format off + "A" SAL_NEWLINE_STRING SAL_NEWLINE_STRING + "- B" SAL_NEWLINE_STRING SAL_NEWLINE_STRING + // indent is 2 spaces + " - C" SAL_NEWLINE_STRING SAL_NEWLINE_STRING + "D" SAL_NEWLINE_STRING SAL_NEWLINE_STRING + "1. E" SAL_NEWLINE_STRING SAL_NEWLINE_STRING + "2. F" SAL_NEWLINE_STRING SAL_NEWLINE_STRING + // indent is 3 spaces + " 1. G" SAL_NEWLINE_STRING + // clang-format on + ); + // Without the accompanying fix in place, this test would have failed, all the "- " and "1. " + // style prefixes were lost. + CPPUNIT_ASSERT_EQUAL(aExpected, aActual); +} + CPPUNIT_PLUGIN_IMPLEMENT(); /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/sw/source/filter/md/wrtmd.cxx b/sw/source/filter/md/wrtmd.cxx index 4021762510df..ec5acca16aa0 100644 --- a/sw/source/filter/md/wrtmd.cxx +++ b/sw/source/filter/md/wrtmd.cxx @@ -29,6 +29,7 @@ #include <sax/tools/converter.hxx> #include <svl/itemiter.hxx> #include <editeng/fontitem.hxx> +#include <comphelper/string.hxx> #include <officecfg/Office/Writer.hxx> @@ -406,8 +407,32 @@ void OutMarkdown_SwTextNode(SwMDWriter& rWrt, const SwTextNode& rNode, bool bFir rWrt.Strm().WriteUniOrByteChar('#'); rWrt.Strm().WriteUniOrByteChar(' '); } + else if (rNode.GetNumRule()) + { + // <https://spec.commonmark.org/0.31.2/#list-items>, the amount of indent we have to use + // depends on the parent's prefix size. + OUStringBuffer aLevel; + auto it = rWrt.GetListLevelPrefixSizes().find(rNode.GetActualListLevel() - 1); + if (it != rWrt.GetListLevelPrefixSizes().end()) + { + comphelper::string::padToLength(aLevel, it->second, ' '); + } + + // In "1." form, should be one of "1." or "1)". + OUString aNumString(rNode.GetNumString()); + if (aNumString.isEmpty() && rNode.HasBullet()) + { + // Should be one of -, +, or *. + aNumString = u"-"_ustr; + } - // TODO: handle lists + if (!aLevel.isEmpty() || !aNumString.isEmpty()) + { + OUString aPrefix = aLevel + aNumString + " "; + rWrt.Strm().WriteUnicodeOrByteText(aPrefix); + rWrt.SetListLevelPrefixSize(rNode.GetActualListLevel(), aPrefix.getLength()); + } + } sal_Int32 nStrPos = rWrt.m_pCurrentPam->GetPoint()->GetContentIndex(); sal_Int32 nEnd = rNodeText.getLength(); @@ -617,6 +642,11 @@ void SwMDWriter::Out_SwDoc(SwPaM* pPam) m_bWriteAll = bSaveWriteAll; // reset to old values } +void SwMDWriter::SetListLevelPrefixSize(int nListLevel, int nPrefixSize) +{ + m_aListLevelPrefixSizes[nListLevel] = nPrefixSize; +} + void GetMDWriter(std::u16string_view /*rFilterOptions*/, const OUString& rBaseURL, WriterRef& xRet) { xRet = new SwMDWriter(rBaseURL); diff --git a/sw/source/filter/md/wrtmd.hxx b/sw/source/filter/md/wrtmd.hxx index 6b26135ea5a5..23490bb96b1b 100644 --- a/sw/source/filter/md/wrtmd.hxx +++ b/sw/source/filter/md/wrtmd.hxx @@ -21,6 +21,8 @@ #include <sal/config.h> +#include <map> + #include <rtl/ustring.hxx> #include <shellio.hxx> @@ -33,6 +35,8 @@ public: bool isInTable() const { return m_bOutTable; } SwNodeOffset StartNodeIndex() const { return m_nStartNodeIndex; } + void SetListLevelPrefixSize(int nListLevel, int nPrefixSize); + const std::map<int, int>& GetListLevelPrefixSizes() const { return m_aListLevelPrefixSizes; } protected: ErrCode WriteStream() override; @@ -42,6 +46,8 @@ private: bool m_bOutTable = false; SwNodeOffset m_nStartNodeIndex{ 0 }; + /// List level -> prefix size map, e.g. "1. " size is 3. + std::map<int, int> m_aListLevelPrefixSizes; }; /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/sw/source/uibase/inc/wrtsh.hxx b/sw/source/uibase/inc/wrtsh.hxx index 0e49e2c889ea..5fbd8d1876c7 100644 --- a/sw/source/uibase/inc/wrtsh.hxx +++ b/sw/source/uibase/inc/wrtsh.hxx @@ -340,7 +340,7 @@ typedef bool (SwWrtShell::*FNSimpleMove)(); */ void NumOrBulletOn(bool bNum); // #i29560# void NumOrBulletOff(); // #i29560# - void NumOn(); + SW_DLLPUBLIC void NumOn(); SW_DLLPUBLIC void BulletOn(); //OLE