sw/qa/filter/md/data/image-and-link.md | 1 sw/qa/filter/md/md.cxx | 61 +++++++++++++++++++++++++++++++++ sw/source/filter/md/mdcallbcks.cxx | 12 +++++- sw/source/filter/md/swmd.cxx | 15 ++++++-- sw/source/filter/md/swmd.hxx | 3 + sw/source/filter/md/wrtmd.cxx | 33 ++++++++++++++++- 6 files changed, 117 insertions(+), 8 deletions(-)
New commits: commit 01fd17ba378b7da7545d29ecbfd883c21a4c6fe4 Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Thu Sep 11 08:36:04 2025 +0200 Commit: Caolán McNamara <caolan.mcnam...@collabora.com> CommitDate: Thu Sep 11 10:28:16 2025 +0200 tdf#168341 sw markdown filter: handle links on images The bugdoc has an image which has a link: the link is lost on both import and export. The doc model for links is different for text an images: the text is covered by an SwFormatINetFormat hint, but images have their SwFlyFrameFormat, and that can contain an SwFormatURL. Fix the problem by looking at the currently pending attributes stack while inserting images: if a link is open, also set that on the image. The original link will be discarded, because it covers no text. Also fix the export to write markup when the fly format has an SwFormatURL. Change-Id: Ib94bcd0903835d6eb197ef2077075390305f37c8 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/190787 Reviewed-by: Caolán McNamara <caolan.mcnam...@collabora.com> Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> diff --git a/sw/qa/filter/md/data/image-and-link.md b/sw/qa/filter/md/data/image-and-link.md new file mode 100644 index 000000000000..75111ca945d5 --- /dev/null +++ b/sw/qa/filter/md/data/image-and-link.md @@ -0,0 +1 @@ +A [](https://www.example.com) Z diff --git a/sw/qa/filter/md/md.cxx b/sw/qa/filter/md/md.cxx index 4de7d058ded0..5123f9172ecd 100644 --- a/sw/qa/filter/md/md.cxx +++ b/sw/qa/filter/md/md.cxx @@ -25,6 +25,7 @@ #include <ndgrf.hxx> #include <itabenum.hxx> #include <ndtxt.hxx> +#include <fmturl.hxx> namespace { @@ -505,6 +506,66 @@ CPPUNIT_TEST_FIXTURE(Test, testTableColumnAdjustMdExport) CPPUNIT_ASSERT_EQUAL(aExpected, aActual); } +CPPUNIT_TEST_FIXTURE(Test, testImageLinkMdImport) +{ + // Given a document with an image which has a link on it: + // When importing that document: + setImportFilterName("Markdown"); + createSwDoc("image-and-link.md"); + + // Then make sure the link is not lost: + SwDocShell* pDocShell = getSwDocShell(); + SwDoc* pDoc = pDocShell->GetDoc(); + sw::FrameFormats<sw::SpzFrameFormat*>& rFlys = *pDoc->GetSpzFrameFormats(); + CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), rFlys.size()); + sw::SpzFrameFormat& rFly = *rFlys[0]; + const SwFormatURL& rURL = rFly.GetURL(); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: https://www.example.com/ + // - Actual : + // i.e. the image's item set didn't have a URL. + CPPUNIT_ASSERT_EQUAL(u"https://www.example.com/"_ustr, rURL.GetURL()); +} + +CPPUNIT_TEST_FIXTURE(Test, testImageLinkMdExport) +{ + // Given a document with an inline, linked image + link 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); + SwNodeOffset nContentOffset = pFlyFormat->GetContent().GetContentIdx()->GetIndex(); + SwGrfNode* pGrfNode = pDoc->GetNodes()[nContentOffset + 1]->GetGrfNode(); + pGrfNode->SetTitle(u"mytitle"_ustr); + SwFormatURL aFormatURL; + aFormatURL.SetURL(u"https://x.com"_ustr, /*bServerMap=*/false); + pFlyFormat->SetFormatAttr(aFormatURL); + pWrtShell->Insert(u" B"_ustr); + + // When saving that to markdown: + save(mpFilter); + + // 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); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: A [](https://x.com) B + // - Actual : A  B + // i.e. the image link 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/mdcallbcks.cxx b/sw/source/filter/md/mdcallbcks.cxx index f37a12ae5175..77878e387975 100644 --- a/sw/source/filter/md/mdcallbcks.cxx +++ b/sw/source/filter/md/mdcallbcks.cxx @@ -177,7 +177,17 @@ int SwMarkdownParser::enter_span_callback(MD_SPANTYPE type, void* detail, void* RTL_TEXTENCODING_UTF8); OUString aTitle = rtl::OStringToOUString(std::string_view(rTitle.text, rTitle.size), RTL_TEXTENCODING_UTF8); - parser->InsertImage(aURL, aTitle); + + const SwFormatINetFormat* pINetFormat = nullptr; + if (!parser->m_aAttrStack.empty() + && parser->m_aAttrStack.back()->Which() == RES_TXTATR_INETFMT) + { + // Copy the link to the image, so it won't be lost for empty spans. + const SfxPoolItem* pTopItem = parser->m_aAttrStack.back().get(); + pINetFormat = dynamic_cast<const SwFormatINetFormat*>(pTopItem); + } + + parser->InsertImage(aURL, aTitle, pINetFormat); parser->m_bInsideImage = true; break; } diff --git a/sw/source/filter/md/swmd.cxx b/sw/source/filter/md/swmd.cxx index 8218a4047632..8b2c829989f9 100644 --- a/sw/source/filter/md/swmd.cxx +++ b/sw/source/filter/md/swmd.cxx @@ -46,6 +46,7 @@ #include <ndgrf.hxx> #include <fmtcntnt.hxx> #include <swtypes.hxx> +#include <fmturl.hxx> #include "swmd.hxx" @@ -592,7 +593,8 @@ void SwMarkdownParser::SetAttrs(SwPaM& rRange) void SwMarkdownParser::ClearAttrs() { m_xDoc->ResetAttrs(*m_pPam, true); } -void SwMarkdownParser::InsertImage(const OUString& aURL, const OUString& rTitle) +void SwMarkdownParser::InsertImage(const OUString& aURL, const OUString& rTitle, + const SwFormatINetFormat* pINetFormat) { OUString sGrfNm = INetURLObject::GetAbsURL(m_sBaseURL, aURL); @@ -636,8 +638,7 @@ void SwMarkdownParser::InsertImage(const OUString& aURL, const OUString& rTitle) } SfxItemSet aFlySet( - SfxItemSet::makeFixedSfxItemSet<RES_FRM_SIZE, RES_VERT_ORIENT, RES_HORI_ORIENT, RES_ANCHOR>( - m_xDoc->GetAttrPool())); + SfxItemSet::makeFixedSfxItemSet<RES_FRMATR_BEGIN, RES_FRMATR_END>(m_xDoc->GetAttrPool())); aFlySet.Put(SwFormatAnchor(RndStdIds::FLY_AS_CHAR)); aFlySet.Put(SwFormatFrameSize(SwFrameSize::Fixed, nWidth, nHeight)); @@ -645,6 +646,14 @@ void SwMarkdownParser::InsertImage(const OUString& aURL, const OUString& rTitle) aFlySet.Put( SwFormatVertOrient(0, text::VertOrientation::CHAR_CENTER, text::RelOrientation::CHAR)); + if (pINetFormat) + { + // Have a link, set that on the image. + SwFormatURL aFormatURL; + aFormatURL.SetURL(pINetFormat->GetValue(), /*bServerMap=*/false); + aFlySet.Put(aFormatURL); + } + SanitizeAnchor(aFlySet); SwFlyFrameFormat* pFlyFormat = m_xDoc->getIDocumentContentOperations().InsertGraphic( diff --git a/sw/source/filter/md/swmd.hxx b/sw/source/filter/md/swmd.hxx index 508da0762d13..a2930de1feae 100644 --- a/sw/source/filter/md/swmd.hxx +++ b/sw/source/filter/md/swmd.hxx @@ -117,7 +117,8 @@ class SwMarkdownParser void SetAttrs(SwPaM& rRange); void ClearAttrs(); - void InsertImage(const OUString& aURL, const OUString& rTitle); + void InsertImage(const OUString& aURL, const OUString& rTitle, + const SwFormatINetFormat* pINetFormat); void StartTable(sal_Int32 nRow, sal_Int32 nCol); void EndTable(); diff --git a/sw/source/filter/md/wrtmd.cxx b/sw/source/filter/md/wrtmd.cxx index 48134875b499..36a28b24289d 100644 --- a/sw/source/filter/md/wrtmd.cxx +++ b/sw/source/filter/md/wrtmd.cxx @@ -47,6 +47,7 @@ #include <charatr.hxx> #include <fmtcntnt.hxx> #include <ndgrf.hxx> +#include <fmturl.hxx> #include "wrtmd.hxx" #include <algorithm> @@ -60,10 +61,12 @@ struct SwMDImageInfo { OUString aURL; OUString aTitle; + OUString aLink; - SwMDImageInfo(const OUString& rURL, const OUString& rTitle) + SwMDImageInfo(const OUString& rURL, const OUString& rTitle, const OUString& rLink) : aURL(rURL) , aTitle(rTitle) + , aLink(rLink) { } @@ -73,7 +76,11 @@ struct SwMDImageInfo return true; if (rOther.aURL < aURL) return false; - return aTitle < rOther.aTitle; + if (aLink < rOther.aLink) + return true; + if (rOther.aLink < aLink) + return false; + return aLink < rOther.aLink; } }; @@ -209,7 +216,13 @@ void ApplyItem(SwMDWriter& rWrt, FormattingStatus& rChange, const SfxPoolItem& r aGraphicURL = URIHelper::simpleNormalizedMakeRelative(rBaseURL, aGraphicURL); } OUString aTitle = pGrfNode->GetTitle(); - rChange.aImages.emplace(aGraphicURL, aTitle); + OUString aLink; + if (rFrameFormat.GetAttrSet().HasItem(RES_URL)) + { + const SwFormatURL& rLink = rFrameFormat.GetURL(); + aLink = rLink.GetURL(); + } + rChange.aImages.emplace(aGraphicURL, aTitle, aLink); break; } } @@ -369,11 +382,25 @@ void OutFormattingChange(SwMDWriter& rWrt, NodePositions& positions, sal_Int32 p continue; } + if (!rImageInfo.aLink.isEmpty()) + { + // Start image link. + rWrt.Strm().WriteUnicodeOrByteText(u"["); + } + rWrt.Strm().WriteUnicodeOrByteText(u"; rWrt.Strm().WriteUnicodeOrByteText(rImageInfo.aURL); rWrt.Strm().WriteUnicodeOrByteText(u")"); + + if (!rImageInfo.aLink.isEmpty()) + { + // End image link. + rWrt.Strm().WriteUnicodeOrByteText(u"]("); + rWrt.Strm().WriteUnicodeOrByteText(rImageInfo.aLink); + rWrt.Strm().WriteUnicodeOrByteText(u")"); + } } current = result;