sw/qa/filter/md/md.cxx | 53 +++++++++++++++++++++++++++++++++--------- sw/source/filter/md/wrtmd.cxx | 31 +++++++++++++++++++----- 2 files changed, 66 insertions(+), 18 deletions(-)
New commits: commit 3a294f97a9b37cc8ff38f76200b5b58879a3a72f Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Fri Sep 19 08:38:56 2025 +0200 Commit: Miklos Vajna <vmik...@collabora.com> CommitDate: Fri Sep 19 12:35:47 2025 +0200 tdf#168446 sw markdown export: improve image name/description/title handling Save the bugdoc back into markdown: the image title is exported as a description, the original description is lost. This was already wrong on the import side, but that is sorted out since commit 412c0391b56419bea6b0ff7c949ef2ced59a4d6b (tdf#168446 Unique name for images and better image representation, 2025-09-18). Now that we have both the title and the description in the model, improve OutFormattingChange() to write both. Note that the `` markup works even with an empty description, while an empty title means that the ` "..."` wrapper around the title has to be omitted, too. See <https://spec.commonmark.org/0.31.2/#images>. Change-Id: Iadc64f28ad1069dcd6f7b14df67b0304d022d835 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/191184 Reviewed-by: Miklos Vajna <vmik...@collabora.com> Tested-by: Jenkins diff --git a/sw/qa/filter/md/md.cxx b/sw/qa/filter/md/md.cxx index 1290fc0786b1..27e857e0b7a1 100644 --- a/sw/qa/filter/md/md.cxx +++ b/sw/qa/filter/md/md.cxx @@ -296,9 +296,7 @@ CPPUNIT_TEST_FIXTURE(Test, testExportingImage) SwFlyFrameFormat* pFlyFormat = rIDCO.InsertGraphic(*pCursor, aGraphicURL, OUString(), &aGraphic, &aFrameSet, /*pGrfAttrSet=*/nullptr, /*SwFrameFormat=*/nullptr); - SwNodeOffset nContentOffset = pFlyFormat->GetContent().GetContentIdx()->GetIndex(); - SwGrfNode* pGrfNode = pDoc->GetNodes()[nContentOffset + 1]->GetGrfNode(); - pGrfNode->SetTitle(u"mytitle"_ustr); + pFlyFormat->SetObjDescription(u"mydesc"_ustr); pWrtShell->Insert(u" B"_ustr); // When saving that to markdown: @@ -306,9 +304,9 @@ CPPUNIT_TEST_FIXTURE(Test, testExportingImage) // Then make sure the image is exported: std::string aActual = TempFileToString(); - std::string aExpected("A  B" SAL_NEWLINE_STRING); + std::string aExpected("A  B" SAL_NEWLINE_STRING); // Without the accompanying fix in place, this test would have failed with: - // - Expected: A  B + // - Expected: A  B // - Actual : A B // i.e. the image was lost. CPPUNIT_ASSERT_EQUAL(aExpected, aActual); @@ -545,9 +543,7 @@ CPPUNIT_TEST_FIXTURE(Test, testImageLinkMdExport) SwFlyFrameFormat* pFlyFormat = rIDCO.InsertGraphic(*pCursor, aGraphicURL, OUString(), &aGraphic, &aFrameSet, /*pGrfAttrSet=*/nullptr, /*SwFrameFormat=*/nullptr); - SwNodeOffset nContentOffset = pFlyFormat->GetContent().GetContentIdx()->GetIndex(); - SwGrfNode* pGrfNode = pDoc->GetNodes()[nContentOffset + 1]->GetGrfNode(); - pGrfNode->SetTitle(u"mytitle"_ustr); + pFlyFormat->SetObjDescription(u"mydesc"_ustr); SwFormatURL aFormatURL; aFormatURL.SetURL(u"https://x.com"_ustr, /*bServerMap=*/false); pFlyFormat->SetFormatAttr(aFormatURL); @@ -558,10 +554,10 @@ CPPUNIT_TEST_FIXTURE(Test, testImageLinkMdExport) // Then make sure the image is exported and the link is not lost: std::string aActual = TempFileToString(); - std::string aExpected("A [](https://x.com) B" SAL_NEWLINE_STRING); + std::string aExpected("A [](https://x.com) B" SAL_NEWLINE_STRING); // Without the accompanying fix in place, this test would have failed with: - // - Expected: A [](https://x.com) B - // - Actual : A  B + // - Expected: A [](https://x.com) B + // - Actual : A  B // i.e. the image link was lost. CPPUNIT_ASSERT_EQUAL(aExpected, aActual); } @@ -588,6 +584,41 @@ CPPUNIT_TEST_FIXTURE(Test, testNewlineMdExport) CPPUNIT_ASSERT_EQUAL(aExpected, aActual); } +CPPUNIT_TEST_FIXTURE(Test, testImageDescTitleExport) +{ + // Given a document with an inline, linked image + desc/title on it: + createSwDoc(); + SwDocShell* pDocShell = getSwDocShell(); + SwDoc* pDoc = pDocShell->GetDoc(); + SwWrtShell* pWrtShell = pDocShell->GetWrtShell(); + pWrtShell->Insert(u"A "_ustr); + SfxItemSet aFrameSet(pDoc->GetAttrPool(), svl::Items<RES_FRMATR_BEGIN, RES_FRMATR_END - 1>); + SwFormatAnchor aAnchor(RndStdIds::FLY_AS_CHAR); + aFrameSet.Put(aAnchor); + Graphic aGraphic; + OUString aGraphicURL(u"./test.png"_ustr); + IDocumentContentOperations& rIDCO = pDoc->getIDocumentContentOperations(); + SwCursor* pCursor = pWrtShell->GetCursor(); + SwFlyFrameFormat* pFlyFormat + = rIDCO.InsertGraphic(*pCursor, aGraphicURL, OUString(), &aGraphic, &aFrameSet, + /*pGrfAttrSet=*/nullptr, /*SwFrameFormat=*/nullptr); + pFlyFormat->SetObjDescription(u"mydesc"_ustr); + pFlyFormat->SetObjTitle(u"mytitle"_ustr); + pWrtShell->Insert(u" B"_ustr); + + // When saving that to markdown: + save(mpFilter); + + // Then make sure the image is exported and the desc/title is not lost: + std::string aActual = TempFileToString(); + std::string aExpected("A  B" SAL_NEWLINE_STRING); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: A  B + // - Actual : A  B + // i.e. the title was exported as a description; the description was 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 623422c5e524..fa53f01d8024 100644 --- a/sw/source/filter/md/wrtmd.cxx +++ b/sw/source/filter/md/wrtmd.cxx @@ -61,11 +61,14 @@ struct SwMDImageInfo { OUString aURL; OUString aTitle; + OUString aDescription; OUString aLink; - SwMDImageInfo(const OUString& rURL, const OUString& rTitle, const OUString& rLink) + SwMDImageInfo(const OUString& rURL, const OUString& rTitle, const OUString& rDescription, + const OUString& rLink) : aURL(rURL) , aTitle(rTitle) + , aDescription(rDescription) , aLink(rLink) { } @@ -76,9 +79,13 @@ struct SwMDImageInfo return true; if (rOther.aURL < aURL) return false; - if (aLink < rOther.aLink) + if (aTitle < rOther.aTitle) return true; - if (rOther.aLink < aLink) + if (rOther.aTitle < aTitle) + return false; + if (aDescription < rOther.aDescription) + return true; + if (rOther.aDescription < aDescription) return false; return aLink < rOther.aLink; } @@ -206,7 +213,8 @@ void ApplyItem(SwMDWriter& rWrt, FormattingStatus& rChange, const SfxPoolItem& r { // Inline image. const SwFormatFlyCnt& rFormatFlyCnt = rItem.StaticWhichCast(RES_TXTATR_FLYCNT); - const SwFrameFormat& rFrameFormat = *rFormatFlyCnt.GetFrameFormat(); + const auto& rFrameFormat + = static_cast<const SwFlyFrameFormat&>(*rFormatFlyCnt.GetFrameFormat()); const SwFormatContent& rFlyContent = rFrameFormat.GetContent(); SwNodeOffset nStart = rFlyContent.GetContentIdx()->GetIndex() + 1; SwGrfNode* pGrfNode = rWrt.m_pDoc->GetNodes()[nStart]->GetGrfNode(); @@ -225,14 +233,15 @@ void ApplyItem(SwMDWriter& rWrt, FormattingStatus& rChange, const SfxPoolItem& r { aGraphicURL = URIHelper::simpleNormalizedMakeRelative(rBaseURL, aGraphicURL); } - OUString aTitle = pGrfNode->GetTitle(); + OUString aTitle = rFrameFormat.GetObjTitle(); + OUString aDescription = rFrameFormat.GetObjDescription(); OUString aLink; if (rFrameFormat.GetAttrSet().HasItem(RES_URL)) { const SwFormatURL& rLink = rFrameFormat.GetURL(); aLink = rLink.GetURL(); } - rChange.aImages.emplace(aGraphicURL, aTitle, aLink); + rChange.aImages.emplace(aGraphicURL, aTitle, aDescription, aLink); break; } } @@ -408,9 +417,17 @@ void OutFormattingChange(SwMDWriter& rWrt, NodePositions& positions, sal_Int32 p } rWrt.Strm().WriteUnicodeOrByteText(u"; rWrt.Strm().WriteUnicodeOrByteText(rImageInfo.aURL); + + if (!rImageInfo.aTitle.isEmpty()) + { + rWrt.Strm().WriteUnicodeOrByteText(u" \""); + OutEscapedChars(rWrt, rImageInfo.aTitle); + rWrt.Strm().WriteUnicodeOrByteText(u"\""); + } + rWrt.Strm().WriteUnicodeOrByteText(u")"); if (!rImageInfo.aLink.isEmpty())