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 bcef97f29a7126cb3469066de4bacafaae30f86a Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Fri Aug 29 09:03:06 2025 +0200 Commit: Miklos Vajna <vmik...@collabora.com> CommitDate: Fri Aug 29 13:35:14 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/+/190361 Tested-by: Jenkins Reviewed-by: Miklos Vajna <vmik...@collabora.com> diff --git a/sw/inc/editsh.hxx b/sw/inc/editsh.hxx index 1abfe2c85134..b78e16336ba7 100644 --- a/sw/inc/editsh.hxx +++ b/sw/inc/editsh.hxx @@ -529,7 +529,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 352bc6d35847..94d4fa618df1 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 fccba16353c6..38452c7eb5be 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> @@ -424,8 +425,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(); @@ -635,6 +660,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 5a292fb23a33..73e9e3923e3a 100644 --- a/sw/source/uibase/inc/wrtsh.hxx +++ b/sw/source/uibase/inc/wrtsh.hxx @@ -349,7 +349,7 @@ typedef bool (SwWrtShell::*FNSimpleMove)(); */ void NumOrBulletOn(bool bNum); // #i29560# void NumOrBulletOff(); // #i29560# - void NumOn(); + SW_DLLPUBLIC void NumOn(); SW_DLLPUBLIC void BulletOn(); //OLE