tdf#168172 sw markdown export: handle images
Load the bugdoc, save back to markdown, the image is lost. Markdown doesn't seem to have a way to embed images, also these images are not anchored. So focus on the image type that is created by the markdown import: linked, inline images. This also required extending OutMarkdown_SwTextNode() a bit, because it makes sense to not emit e.g. empty bold or italic hints, but e.g. inline images are hints with no end, but we still want to produce output for them. Change-Id: I6529606ff4dd677815da4668f31c079184e18fa9 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/190475 Reviewed-by: Miklos Vajna <vmiklos@collabora.com> Tested-by: Jenkins
This commit is contained in:
@@ -28,6 +28,7 @@ $(eval $(call gb_CppunitTest_use_libraries,sw_filter_md, \
|
||||
utl \
|
||||
svl \
|
||||
sfx \
|
||||
vcl \
|
||||
))
|
||||
|
||||
$(eval $(call gb_CppunitTest_use_externals,sw_filter_md,\
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ public:
|
||||
virtual bool RestorePersistentData();
|
||||
|
||||
SW_DLLPUBLIC OUString GetTitle() const;
|
||||
void SetTitle( const OUString& rTitle );
|
||||
SW_DLLPUBLIC void SetTitle( const OUString& rTitle );
|
||||
SW_DLLPUBLIC OUString GetDescription() const;
|
||||
void SetDescription( const OUString& rDescription );
|
||||
|
||||
|
||||
+52
-10
@@ -19,6 +19,10 @@
|
||||
#include <IDocumentStylePoolAccess.hxx>
|
||||
#include <poolfmt.hxx>
|
||||
#include <charatr.hxx>
|
||||
#include <fmtanchr.hxx>
|
||||
#include <IDocumentContentOperations.hxx>
|
||||
#include <fmtcntnt.hxx>
|
||||
#include <ndgrf.hxx>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -39,6 +43,14 @@ public:
|
||||
: SwModelTestBase(u"/sw/qa/filter/md/data/"_ustr, u"Markdown"_ustr)
|
||||
{
|
||||
}
|
||||
|
||||
std::string TempFileToString()
|
||||
{
|
||||
SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
|
||||
std::vector<char> aBuffer(aStream.remainingSize());
|
||||
aStream.ReadBytes(aBuffer.data(), aBuffer.size());
|
||||
return std::string(aBuffer.data(), aBuffer.size());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -206,15 +218,12 @@ CPPUNIT_TEST_FIXTURE(Test, testExportingCodeSpan)
|
||||
save(mpFilter);
|
||||
|
||||
// Then make sure the format of B 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 aActual = TempFileToString();
|
||||
// Without the accompanying fix in place, this test would have failed with:
|
||||
// - Expected: A `B` C
|
||||
// - Actual : A B C
|
||||
// i.e. the code formatting was lost.
|
||||
std::string_view aExpected("A `B` C" SAL_NEWLINE_STRING);
|
||||
std::string aExpected("A `B` C" SAL_NEWLINE_STRING);
|
||||
CPPUNIT_ASSERT_EQUAL(aExpected, aActual);
|
||||
}
|
||||
|
||||
@@ -247,11 +256,8 @@ CPPUNIT_TEST_FIXTURE(Test, testExportingList)
|
||||
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(
|
||||
std::string aActual = TempFileToString();
|
||||
std::string aExpected(
|
||||
// clang-format off
|
||||
"A" SAL_NEWLINE_STRING SAL_NEWLINE_STRING
|
||||
"- B" SAL_NEWLINE_STRING SAL_NEWLINE_STRING
|
||||
@@ -269,6 +275,42 @@ CPPUNIT_TEST_FIXTURE(Test, testExportingList)
|
||||
CPPUNIT_ASSERT_EQUAL(aExpected, aActual);
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_FIXTURE(Test, testExportingImage)
|
||||
{
|
||||
// Given a document with an inline, linked image:
|
||||
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);
|
||||
pWrtShell->Insert(u" B"_ustr);
|
||||
|
||||
// When saving that to markdown:
|
||||
save(mpFilter);
|
||||
|
||||
// Then make sure the image is exported:
|
||||
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 image 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: */
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <svl/itemiter.hxx>
|
||||
#include <editeng/fontitem.hxx>
|
||||
#include <comphelper/string.hxx>
|
||||
#include <svl/urihelper.hxx>
|
||||
|
||||
#include <officecfg/Office/Writer.hxx>
|
||||
|
||||
@@ -44,6 +45,8 @@
|
||||
#include <strings.hrc>
|
||||
#include <txatbase.hxx>
|
||||
#include <charatr.hxx>
|
||||
#include <fmtcntnt.hxx>
|
||||
#include <ndgrf.hxx>
|
||||
#include "wrtmd.hxx"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -52,6 +55,28 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
/// Stores information about a to-be-exported linked image.
|
||||
struct SwMDImageInfo
|
||||
{
|
||||
OUString aURL;
|
||||
OUString aTitle;
|
||||
|
||||
SwMDImageInfo(const OUString& rURL, const OUString& rTitle)
|
||||
: aURL(rURL)
|
||||
, aTitle(rTitle)
|
||||
{
|
||||
}
|
||||
|
||||
bool operator<(const SwMDImageInfo& rOther) const
|
||||
{
|
||||
if (aURL < rOther.aURL)
|
||||
return true;
|
||||
if (rOther.aURL < aURL)
|
||||
return false;
|
||||
return aTitle < rOther.aTitle;
|
||||
}
|
||||
};
|
||||
|
||||
// This struct contains a state of the properties that we output. The integers represent "level"
|
||||
// of a given feature. Positive values mean applied status; zero means cleared status. Sometimes
|
||||
// values may be negative: e.g., when a node started, and some redline continues from a previous
|
||||
@@ -66,6 +91,7 @@ struct FormattingStatus
|
||||
int nCodeChange = 0;
|
||||
std::unordered_map<OUString, int> aHyperlinkChanges;
|
||||
std::unordered_map<const SwRangeRedline*, int> aRedlineChanges;
|
||||
std::set<SwMDImageInfo> aImages;
|
||||
};
|
||||
|
||||
// This is a vector of positions in the node text, where objects of class T start or end.
|
||||
@@ -169,6 +195,33 @@ void ApplyItem(SwMDWriter& rWrt, FormattingStatus& rChange, const SfxPoolItem& r
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RES_TXTATR_FLYCNT:
|
||||
{
|
||||
// Inline image.
|
||||
const SwFormatFlyCnt& rFormatFlyCnt = rItem.StaticWhichCast(RES_TXTATR_FLYCNT);
|
||||
const SwFrameFormat& rFrameFormat = *rFormatFlyCnt.GetFrameFormat();
|
||||
const SwFormatContent& rFlyContent = rFrameFormat.GetContent();
|
||||
SwNodeOffset nStart = rFlyContent.GetContentIdx()->GetIndex() + 1;
|
||||
SwGrfNode* pGrfNode = rWrt.m_pDoc->GetNodes()[nStart]->GetGrfNode();
|
||||
Graphic aGraphic = pGrfNode->GetGraphic();
|
||||
if (!pGrfNode->IsLinkedFile())
|
||||
{
|
||||
// Not linked, ignore for now.
|
||||
break;
|
||||
}
|
||||
|
||||
// Try to extract a relative URL and a title.
|
||||
OUString aGraphicURL;
|
||||
pGrfNode->GetFileFilterNms(&aGraphicURL, /*pFilterNm=*/nullptr);
|
||||
const OUString& rBaseURL = rWrt.GetBaseURL();
|
||||
if (!rBaseURL.isEmpty())
|
||||
{
|
||||
aGraphicURL = URIHelper::simpleNormalizedMakeRelative(rBaseURL, aGraphicURL);
|
||||
}
|
||||
OUString aTitle = pGrfNode->GetTitle();
|
||||
rChange.aImages.emplace(aGraphicURL, aTitle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +266,8 @@ FormattingStatus CalculateFormattingChange(SwMDWriter& rWrt, NodePositions& posi
|
||||
bool ShouldCloseIt(int prev, int curr) { return prev != curr && prev >= 0 && curr <= 0; }
|
||||
bool ShouldOpenIt(int prev, int curr) { return prev != curr && prev <= 0 && curr > 0; }
|
||||
|
||||
void OutEscapedChars(SwMDWriter& rWrt, std::u16string_view chars);
|
||||
|
||||
// Calculate the updated status of properties. Then compare the status before and after; and for
|
||||
// each event when a value changes from 0 to a positive number, output respective opening markup;
|
||||
// when a value is decreased to 0, a closing markup is written. For redlines, closing markup is
|
||||
@@ -323,6 +378,23 @@ void OutFormattingChange(SwMDWriter& rWrt, NodePositions& positions, sal_Int32 p
|
||||
rWrt.Strm().WriteUnicodeOrByteText(u"`");
|
||||
}
|
||||
|
||||
// Images: write the complete markup on start.
|
||||
for (const auto& rImageInfo : result.aImages)
|
||||
{
|
||||
// 'current' is the old state and 'result' is the new state, so only write images which are
|
||||
// new in 'result'.
|
||||
if (current.aImages.contains(rImageInfo))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
rWrt.Strm().WriteUnicodeOrByteText(u";
|
||||
rWrt.Strm().WriteUnicodeOrByteText(rImageInfo.aURL);
|
||||
rWrt.Strm().WriteUnicodeOrByteText(u")");
|
||||
}
|
||||
|
||||
current = std::move(result);
|
||||
}
|
||||
|
||||
@@ -472,7 +544,13 @@ void OutMarkdown_SwTextNode(SwMDWriter& rWrt, const SwTextNode& rNode, bool bFir
|
||||
if (nHintStart >= nEnd)
|
||||
break;
|
||||
const sal_Int32 nHintEnd = pHint->GetAnyEnd();
|
||||
if (nHintEnd == nHintStart || nHintEnd <= nStrPos)
|
||||
if (nHintStart >= nStrPos && pHint->Which() == RES_TXTATR_FLYCNT)
|
||||
{
|
||||
// SwTextFlyCnt is a hint that has no end, but still output it.
|
||||
positions.hintStarts.add(std::max(nHintStart, nStrPos), &pHint->GetAttr());
|
||||
continue;
|
||||
}
|
||||
else if (nHintEnd == nHintStart || nHintEnd <= nStrPos)
|
||||
continue; // no output of zero-length hints and hints ended before output started yet
|
||||
positions.hintStarts.add(std::max(nHintStart, nStrPos), &pHint->GetAttr());
|
||||
positions.hintEnds.add(std::min(nHintEnd, nEnd), &pHint->GetAttr());
|
||||
|
||||
Reference in New Issue
Block a user