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 ++++++++++++++++- sw/source/uibase/utlui/uitool.cxx | 2 - 7 files changed, 118 insertions(+), 9 deletions(-)
New commits: commit 3afb246da376076c7d05d282c48f274b3d96d029 Author: Xisco Fauli <[email protected]> AuthorDate: Thu Sep 11 22:38:26 2025 +0200 Commit: Xisco Fauli <[email protected]> CommitDate: Fri Sep 12 08:52:40 2025 +0200 sw: silence warning C6011: Dereferencing NULL pointer 'pFormat' Change-Id: I2c075588d6efff4f51832ccefc241e8e135954c0 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/190840 Tested-by: Jenkins Reviewed-by: Xisco Fauli <[email protected]> diff --git a/sw/source/uibase/utlui/uitool.cxx b/sw/source/uibase/utlui/uitool.cxx index 5f98464af353..986fc9d45c7b 100644 --- a/sw/source/uibase/utlui/uitool.cxx +++ b/sw/source/uibase/utlui/uitool.cxx @@ -237,7 +237,7 @@ static void FillHdFt(SfxPoolItem& rFormat, const SfxItemSet& rSet, SwPageDesc& r pFormat = pHeader->GetHeaderFormat(); else pFormat = pFooter->GetFooterFormat(); - OSL_ENSURE(pFormat != nullptr, "no header or footer format"); + assert(pFormat && "no header or footer format"); SwAttrSet aSet(pFormat->GetAttrSet()); aSet.Put(rSet); commit b7bc0e5f3999950d6b5f0d2bdcda2c6cc2f04e61 Author: Miklos Vajna <[email protected]> AuthorDate: Thu Sep 11 08:36:04 2025 +0200 Commit: Miklos Vajna <[email protected]> CommitDate: Fri Sep 12 08:52:33 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/+/190813 Tested-by: Jenkins Reviewed-by: Miklos Vajna <[email protected]> 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 824b80d3874d..1de912e55ed8 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 f8692993dc55..655f95bd344d 100644 --- a/sw/source/filter/md/mdcallbcks.cxx +++ b/sw/source/filter/md/mdcallbcks.cxx @@ -180,7 +180,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 cf687d3a9ea6..1b46ae46494b 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 30e96a626990..d4646ccb507c 100644 --- a/sw/source/filter/md/swmd.hxx +++ b/sw/source/filter/md/swmd.hxx @@ -118,7 +118,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 7430aa389447..d8764a9aa283 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; } }; @@ -219,7 +226,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; } } @@ -388,11 +401,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 = std::move(result);
