sw/qa/filter/md/md.cxx        |   53 +++++++++++++++++++++++++++++++++---------
 sw/source/filter/md/wrtmd.cxx |   31 +++++++++++++++++++-----
 2 files changed, 66 insertions(+), 18 deletions(-)

New commits:
commit de6b70afe5c78af5088c064334e73d547df4e8a8
Author:     Miklos Vajna <vmik...@collabora.com>
AuthorDate: Fri Sep 19 08:38:56 2025 +0200
Commit:     Caolán McNamara <caolan.mcnam...@collabora.com>
CommitDate: Fri Sep 19 10:10:20 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 `![mydesc](url "mytitle")` 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/+/191172
    Reviewed-by: Caolán McNamara <caolan.mcnam...@collabora.com>
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com>

diff --git a/sw/qa/filter/md/md.cxx b/sw/qa/filter/md/md.cxx
index bb6210cb7039..6096c0829b57 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 ![mytitle](./test.png) B" SAL_NEWLINE_STRING);
+    std::string aExpected("A ![mydesc](./test.png) B" SAL_NEWLINE_STRING);
     // Without the accompanying fix in place, this test would have failed with:
-    // - Expected: A ![mytitle](./test.png) B
+    // - Expected: A ![mydesc](./test.png) 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 [![mytitle](./test.png)](https://x.com) B" 
SAL_NEWLINE_STRING);
+    std::string aExpected("A [![mydesc](./test.png)](https://x.com) B" 
SAL_NEWLINE_STRING);
     // Without the accompanying fix in place, this test would have failed with:
-    // - Expected: A [![mytitle](./test.png)](https://x.com) B
-    // - Actual  : A ![mytitle](./test.png) B
+    // - Expected: A [![mydesc](./test.png)](https://x.com) B
+    // - Actual  : A ![mydesc](./test.png) 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 ![mydesc](./test.png \"mytitle\") B" 
SAL_NEWLINE_STRING);
+    // Without the accompanying fix in place, this test would have failed with:
+    // - Expected: A ![mydesc](./test.png "mytitle") B
+    // - Actual  : A ![mytitle](./test.png) 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 38959584adf5..50bf3810d917 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;
     }
@@ -196,7 +203,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();
@@ -215,14 +223,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;
         }
     }
@@ -389,9 +398,17 @@ void OutFormattingChange(SwMDWriter& rWrt, NodePositions& 
positions, sal_Int32 p
         }
 
         rWrt.Strm().WriteUnicodeOrByteText(u"![");
-        OutEscapedChars(rWrt, rImageInfo.aTitle);
+        OutEscapedChars(rWrt, rImageInfo.aDescription);
         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())

Reply via email to