From 8dcb5baf0e912a3df116a7bf9b690f9b6988623d Mon Sep 17 00:00:00 2001 From: Mike Kaganski Date: Fri, 11 Jul 2025 17:31:02 +0500 Subject: [PATCH] tdf#160734: Markdown export: redlines Use HTML and for these. Additionally: * fix paragraph export: an empty line is needed between paragraphs. * Avoid exporting dummy characters. * Use UTF-8 for export. Change-Id: I7d479b39c1650fa9889b230d799f2844ecf85bee Reviewed-on: https://gerrit.libreoffice.org/c/core/+/187704 Tested-by: Jenkins Reviewed-by: Mike Kaganski --- .../filter/md/data/redlines-and-comments.odt | Bin 0 -> 13408 bytes sw/qa/filter/md/md.cxx | 37 +++ sw/source/filter/md/wrtmd.cxx | 240 ++++++++++++++---- 3 files changed, 232 insertions(+), 45 deletions(-) create mode 100644 sw/qa/filter/md/data/redlines-and-comments.odt diff --git a/sw/qa/filter/md/data/redlines-and-comments.odt b/sw/qa/filter/md/data/redlines-and-comments.odt new file mode 100644 index 0000000000000000000000000000000000000000..8fb7a54fb31c76ecd9072277091918e2ad54f681 GIT binary patch literal 13408 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 U1q$}J8^oXA+#fHvGySdoAG(6?E&u=k literal 0 HcmV?d00001 diff --git a/sw/qa/filter/md/md.cxx b/sw/qa/filter/md/md.cxx index 369d3173e00f..c7434c508811 100644 --- a/sw/qa/filter/md/md.cxx +++ b/sw/qa/filter/md/md.cxx @@ -45,15 +45,25 @@ CPPUNIT_TEST_FIXTURE(Test, testExportingBasicElements) std::string_view expected( // clang-format off "# Heading 1" SAL_NEWLINE_STRING + SAL_NEWLINE_STRING "## Heading 2" SAL_NEWLINE_STRING + SAL_NEWLINE_STRING "### Heading 3" SAL_NEWLINE_STRING + SAL_NEWLINE_STRING "#### Heading 4" SAL_NEWLINE_STRING + SAL_NEWLINE_STRING "##### Heading 5" SAL_NEWLINE_STRING + SAL_NEWLINE_STRING "###### Heading 6" SAL_NEWLINE_STRING + SAL_NEWLINE_STRING "**Bold** text" SAL_NEWLINE_STRING + SAL_NEWLINE_STRING "Text in *italics*" SAL_NEWLINE_STRING + SAL_NEWLINE_STRING "This is a [hyperlink](http://www.libreoffice.org/)" SAL_NEWLINE_STRING + SAL_NEWLINE_STRING "\\# Leading hash" SAL_NEWLINE_STRING + SAL_NEWLINE_STRING "Some \\{braces\\}, \\[square brackets\\], \\*asterisks\\*, \\`backticks\\`, \\\\backslashes\\\\, \\_underscores\\_, \\" SAL_NEWLINE_STRING SAL_NEWLINE_STRING // clang-format on @@ -89,6 +99,33 @@ CPPUNIT_TEST_FIXTURE(Test, testList) CPPUNIT_ASSERT_EQUAL(OUString("Unordered"), getParagraph(2)->getString()); } +CPPUNIT_TEST_FIXTURE(Test, testExportingRedlines) +{ + // Given a document with some redlines + createSwDoc("redlines-and-comments.odt"); + + // Save as a markdown document + save(mpFilter); + SvFileStream fileStream(maTempFile.GetURL(), StreamMode::READ); + OUString aParagraph; + // 1st paragraph + CPPUNIT_ASSERT(fileStream.ReadUniOrByteStringLine(aParagraph, RTL_TEXTENCODING_UTF8)); + // Check that the insert/delete redlines were exported as / elements + std::u16string_view expected + = uR"( )"; + CPPUNIT_ASSERT(aParagraph.indexOf(expected) >= 0); + expected = uR"()"; + CPPUNIT_ASSERT(aParagraph.indexOf(expected) >= 0); + // The insert starts on the first paragraph, and ends on the second + CPPUNIT_ASSERT(aParagraph.indexOf("") < 0); + // An empty line between paragraphs + CPPUNIT_ASSERT(fileStream.ReadUniOrByteStringLine(aParagraph, RTL_TEXTENCODING_UTF8)); + CPPUNIT_ASSERT(aParagraph.isEmpty()); + // 2nd paragraph + CPPUNIT_ASSERT(fileStream.ReadUniOrByteStringLine(aParagraph, RTL_TEXTENCODING_UTF8)); + CPPUNIT_ASSERT(aParagraph.indexOf("") >= 0); +} + 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 713ce380dbc5..07315b4274e4 100644 --- a/sw/source/filter/md/wrtmd.cxx +++ b/sw/source/filter/md/wrtmd.cxx @@ -26,14 +26,18 @@ #include #include #include +#include #include #include +#include #include +#include #include #include #include +#include #include #include #include "wrtmd.hxx" @@ -51,20 +55,39 @@ struct FormattingStatus int nUnderlineChange = 0; int nWeightChange = 0; std::unordered_map aHyperlinkChanges; + std::unordered_map aRedlineChanges; }; -struct HintsAtPos +template struct PosData { - using value_type = std::pair; + using value_type = std::pair; + static bool value_less(const value_type& l, const value_type& r) { return l.first < r.first; } std::vector table; size_t cur = 0; const value_type* get(size_t n) const { return n < table.size() ? &table[n] : nullptr; } const value_type* current() const { return get(cur); } const value_type* next() { return get(++cur); } - void sort() + void add(sal_Int32 pos, const T* val) { table.emplace_back(pos, val); } + void sort() { std::stable_sort(table.begin(), table.end(), value_less); } +}; + +struct NodePositions +{ + PosData hintStarts; + PosData hintEnds; + PosData redlineStarts; + PosData redlineEnds; + + sal_Int32 getEndOfCurrent(sal_Int32 end) { - std::stable_sort(table.begin(), table.end(), - [](auto& lh, auto& rh) { return lh.first < rh.first; }); + auto pos_of = [](const auto* v) { return v ? v->first : SAL_MAX_INT32; }; + return std::min({ + end, + pos_of(hintEnds.current()), + pos_of(hintStarts.current()), + pos_of(redlineEnds.current()), + pos_of(redlineStarts.current()), + }); } }; @@ -120,56 +143,70 @@ void ApplyItem(FormattingStatus& rChange, const SfxPoolItem& rItem, int incremen } } -FormattingStatus CalculateFormattingChange(HintsAtPos& starts, HintsAtPos& ends, sal_Int32 pos, +void ApplyItem(FormattingStatus& rChange, const SwRangeRedline* pItem, int increment) +{ + rChange.aRedlineChanges[pItem] += increment; +} + +FormattingStatus CalculateFormattingChange(NodePositions& positions, sal_Int32 pos, const FormattingStatus& currentFormatting) { FormattingStatus result(currentFormatting); // 1. Output closing attributes - for (auto* p = ends.current(); p && p->first == pos; p = ends.next()) + for (auto* p = positions.hintEnds.current(); p && p->first == pos; + p = positions.hintEnds.next()) ApplyItem(result, *p->second, -1); // 2. Output opening attributes - for (auto* p = starts.current(); p && p->first == pos; p = starts.next()) + for (auto* p = positions.hintStarts.current(); p && p->first == pos; + p = positions.hintStarts.next()) ApplyItem(result, *p->second, +1); + // 3. Output closing redlines + for (auto* p = positions.redlineEnds.current(); p && p->first == pos; + p = positions.redlineEnds.next()) + ApplyItem(result, p->second, -1); + + // 4. Output opening redlines + for (auto* p = positions.redlineStarts.current(); p && p->first == pos; + p = positions.redlineStarts.next()) + ApplyItem(result, p->second, +1); + return result; } -void OutFormattingChange(SwMDWriter& rWrt, HintsAtPos& starts, HintsAtPos& ends, sal_Int32 pos, +// Closing redlines may happen in a following paragraph; there it will change from 0 to -1. +// Account for that possibility in ShouldCloseIt. +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 OutFormattingChange(SwMDWriter& rWrt, NodePositions& positions, sal_Int32 pos, FormattingStatus& current) { - FormattingStatus result = CalculateFormattingChange(starts, ends, pos, current); + FormattingStatus result = CalculateFormattingChange(positions, pos, current); + + // Closing stuff + + // TODO/FIXME: the closing characters must be right-flanking // Not in CommonMark - if (current.nCrossedOutChange <= 0 && result.nCrossedOutChange > 0) - rWrt.Strm().WriteUnicodeOrByteText(u"~~"); - else if (current.nCrossedOutChange > 0 && result.nCrossedOutChange <= 0) + if (ShouldCloseIt(current.nCrossedOutChange, result.nCrossedOutChange)) rWrt.Strm().WriteUnicodeOrByteText(u"~~"); - if ((current.nPostureChange <= 0 && result.nPostureChange > 0) - || (current.nPostureChange > 0 && result.nPostureChange <= 0)) - rWrt.Strm().WriteUnicodeOrByteText(u"*"); // both to open, and to close + if (ShouldCloseIt(current.nPostureChange, result.nPostureChange)) + rWrt.Strm().WriteUnicodeOrByteText(u"*"); - if (current.nUnderlineChange <= 0 && result.nUnderlineChange > 0) - { - //rWrt.Strm().WriteUnicodeOrByteText(u"[u]"); - } - else if (current.nUnderlineChange > 0 && result.nUnderlineChange <= 0) + if (ShouldCloseIt(current.nUnderlineChange, result.nUnderlineChange)) { //rWrt.Strm().WriteUnicodeOrByteText(u"[/u]"); } - if ((current.nWeightChange <= 0 && result.nWeightChange > 0) - || (current.nWeightChange > 0 && result.nWeightChange <= 0)) // both to open, and to close + if (ShouldCloseIt(current.nWeightChange, result.nWeightChange)) rWrt.Strm().WriteUnicodeOrByteText(u"**"); - for (const auto & [ url, delta ] : result.aHyperlinkChanges) + for (const auto & [ url, curr ] : result.aHyperlinkChanges) { - if (current.aHyperlinkChanges[url] <= 0 && delta > 0) - { - rWrt.Strm().WriteUnicodeOrByteText(u"["); - } - else if (current.aHyperlinkChanges[url] > 0 && delta <= 0) + if (ShouldCloseIt(current.aHyperlinkChanges[url], curr)) { rWrt.Strm().WriteUnicodeOrByteText(u"]("); rWrt.Strm().WriteUnicodeOrByteText(url); @@ -177,6 +214,68 @@ void OutFormattingChange(SwMDWriter& rWrt, HintsAtPos& starts, HintsAtPos& ends, } } + for (const auto & [ pRedline, curr ] : result.aRedlineChanges) + { + if (ShouldCloseIt(current.aRedlineChanges[pRedline], curr)) + { + // + rWrt.Strm().WriteUnicodeOrByteText(u"GetType() == RedlineType::Insert) + rWrt.Strm().WriteUnicodeOrByteText(u"ins"); + else if (pRedline->GetType() == RedlineType::Delete) + rWrt.Strm().WriteUnicodeOrByteText(u"del"); + rWrt.Strm().WriteUnicodeOrByteText(u">"); + } + } + + // Opening stuff + + // TODO/FIXME: the opening characters must be left-flanking + + for (const auto & [ pRedline, curr ] : result.aRedlineChanges) + { + if (ShouldOpenIt(current.aRedlineChanges[pRedline], curr)) + { + // + rWrt.Strm().WriteUnicodeOrByteText(u"<"); + if (pRedline->GetType() == RedlineType::Insert) + rWrt.Strm().WriteUnicodeOrByteText(u"ins"); + else if (pRedline->GetType() == RedlineType::Delete) + rWrt.Strm().WriteUnicodeOrByteText(u"del"); + rWrt.Strm().WriteUnicodeOrByteText(u" title=\"Author: "); + rWrt.Strm().WriteUnicodeOrByteText(pRedline->GetAuthorString()); + rWrt.Strm().WriteUnicodeOrByteText(u"\" datetime=\""); + OUStringBuffer buf; + sax::Converter::convertDateTime(buf, pRedline->GetTimeStamp().GetUNODateTime(), + nullptr); + rWrt.Strm().WriteUnicodeOrByteText(buf); + rWrt.Strm().WriteUnicodeOrByteText(u"\">"); + } + } + + // Not in CommonMark + if (ShouldOpenIt(current.nCrossedOutChange, result.nCrossedOutChange)) + rWrt.Strm().WriteUnicodeOrByteText(u"~~"); + + if (ShouldOpenIt(current.nPostureChange, result.nPostureChange)) + rWrt.Strm().WriteUnicodeOrByteText(u"*"); + + if (ShouldOpenIt(current.nUnderlineChange, result.nUnderlineChange)) + { + //rWrt.Strm().WriteUnicodeOrByteText(u"[u]"); + } + + if (ShouldOpenIt(current.nWeightChange, result.nWeightChange)) + rWrt.Strm().WriteUnicodeOrByteText(u"**"); + + for (const auto & [ url, curr ] : result.aHyperlinkChanges) + { + if (ShouldOpenIt(current.aHyperlinkChanges[url], curr)) + { + rWrt.Strm().WriteUnicodeOrByteText(u"["); + } + } + current = std::move(result); } @@ -188,6 +287,20 @@ void OutEscapedChars(SwMDWriter& rWrt, std::u16string_view chars) sal_uInt32 ch = o3tl::iterateCodePoints(chars, &pos); switch (ch) { + // dummy characters: anchors, comments, etc. TODO: handle their attributes / content. + case CH_TXTATR_BREAKWORD: + case CH_TXTATR_INWORD: + case CH_TXT_ATR_INPUTFIELDSTART: + case CH_TXT_ATR_INPUTFIELDEND: + case CH_TXT_ATR_FORMELEMENT: + case CH_TXT_ATR_FIELDSTART: + case CH_TXT_ATR_FIELDSEP: + case CH_TXT_ATR_FIELDEND: + case CH_TXT_TRACKED_DUMMY_CHAR: + break; + + // TODO: line breaks + case '\\': case '`': case '*': @@ -209,11 +322,15 @@ void OutEscapedChars(SwMDWriter& rWrt, std::u16string_view chars) } /* Output of the nodes*/ -void OutMarkdown_SwTextNode(SwMDWriter& rWrt, const SwTextNode& rNode) +void OutMarkdown_SwTextNode(SwMDWriter& rWrt, const SwTextNode& rNode, bool bFirst) { const OUString& rNodeText = rNode.GetText(); if (!rNodeText.isEmpty()) { + // Paragraphs separate by empty lines + if (!bFirst) + rWrt.Strm().WriteUnicodeOrByteText(u"" SAL_NEWLINE_STRING); + int nHeadingLevel = 0; for (const SwFormat* pFormat = &rNode.GetAnyFormatColl(); pFormat; pFormat = pFormat->DerivedFrom()) @@ -262,16 +379,18 @@ void OutMarkdown_SwTextNode(SwMDWriter& rWrt, const SwTextNode& rNode) rWrt.Strm().WriteUniOrByteChar(' '); } + // TODO: handle lists + sal_Int32 nStrPos = rWrt.m_pCurrentPam->GetPoint()->GetContentIndex(); sal_Int32 nEnd = rNodeText.getLength(); if (rWrt.m_pCurrentPam->GetPoint()->GetNode() == rWrt.m_pCurrentPam->GetMark()->GetNode()) nEnd = rWrt.m_pCurrentPam->GetMark()->GetContentIndex(); - HintsAtPos aHintStarts, aHintEnds; + NodePositions positions; // Start paragraph properties for (SfxItemIter iter(rNode.GetSwAttrSet()); !iter.IsAtEnd(); iter.NextItem()) - aHintStarts.table.emplace_back(nStrPos, iter.GetCurItem()); + positions.hintStarts.add(nStrPos, iter.GetCurItem()); // Store character formatting const size_t nCntAttr = rNode.HasHints() ? rNode.GetSwpHints().Count() : 0; @@ -284,40 +403,70 @@ void OutMarkdown_SwTextNode(SwMDWriter& rWrt, const SwTextNode& rNode) const sal_Int32 nHintEnd = pHint->GetAnyEnd(); if (nHintEnd == nHintStart || nHintEnd <= nStrPos) continue; // no output of zero-length hints and hints ended before output started yet - aHintStarts.table.emplace_back(std::max(nHintStart, nStrPos), &pHint->GetAttr()); - aHintEnds.table.emplace_back(std::min(nHintEnd, nEnd), &pHint->GetAttr()); + positions.hintStarts.add(std::max(nHintStart, nStrPos), &pHint->GetAttr()); + positions.hintEnds.add(std::min(nHintEnd, nEnd), &pHint->GetAttr()); } - aHintEnds.sort(); + positions.hintEnds.sort(); + // End paragraph properties for (SfxItemIter iter(rNode.GetSwAttrSet()); !iter.IsAtEnd(); iter.NextItem()) - aHintEnds.table.emplace_back(nEnd, iter.GetCurItem()); + positions.hintEnds.add(nEnd, iter.GetCurItem()); + + if (const SwRedlineTable& rRedlines + = rNode.GetDoc().getIDocumentRedlineAccess().GetRedlineTable(); + !rRedlines.empty() && rRedlines.GetMaxEndPos() >= SwPosition(rNode)) + { + for (const SwRangeRedline* pRedline : rRedlines) + { + const auto[redlineStart, redlineEnd] = pRedline->StartEnd(); + if (redlineStart->GetContentNode()->GetIndex() > rNode.GetIndex() + || (redlineStart->GetContentNode()->GetIndex() == rNode.GetIndex() + && redlineStart->GetContentIndex() > nEnd)) + break; + if (redlineEnd->GetContentNode()->GetIndex() < rNode.GetIndex() + || (redlineEnd->GetContentNode()->GetIndex() == rNode.GetIndex() + && redlineEnd->GetContentIndex() < nStrPos)) + continue; + + if (pRedline->GetType() != RedlineType::Insert + && pRedline->GetType() != RedlineType::Delete) + continue; + + if (*redlineStart->GetContentNode() == rNode + && redlineStart->GetContentIndex() >= nStrPos) + positions.redlineStarts.add(redlineStart->GetContentIndex(), pRedline); + + if (*redlineEnd->GetContentNode() == rNode && redlineEnd->GetContentIndex() <= nEnd) + positions.redlineEnds.add(redlineEnd->GetContentIndex(), pRedline); + } + } + + positions.redlineEnds.sort(); FormattingStatus currentStatus; while (nStrPos < nEnd) { // 1. Output attributes - OutFormattingChange(rWrt, aHintStarts, aHintEnds, nStrPos, currentStatus); + OutFormattingChange(rWrt, positions, nStrPos, currentStatus); // 2. Escape and output the character. This relies on hints not appearing in the middle of // a surrogate pair. - sal_Int32 nEndOfChunk = nEnd; - if (auto* p = aHintEnds.current(); p && p->first < nEndOfChunk) - nEndOfChunk = p->first; - if (auto* p = aHintStarts.current(); p && p->first < nEndOfChunk) - nEndOfChunk = p->first; + sal_Int32 nEndOfChunk = positions.getEndOfCurrent(nEnd); OutEscapedChars(rWrt, rNodeText.subView(nStrPos, nEndOfChunk - nStrPos)); nStrPos = nEndOfChunk; } - assert(aHintStarts.current() == nullptr); + assert(positions.hintStarts.current() == nullptr); // Output final closing attributes - OutFormattingChange(rWrt, aHintStarts, aHintEnds, nEnd, currentStatus); + OutFormattingChange(rWrt, positions, nEnd, currentStatus); } rWrt.Strm().WriteUnicodeOrByteText(u"" SAL_NEWLINE_STRING); } void OutMarkdown_SwTableNode(SwMDWriter& /*rWrt*/, const SwTableNode& /*rNode*/) { + // TODO + //const SwTable& rTable = rNode.GetTable(); //WriterRef pHtmlWrt; @@ -335,6 +484,7 @@ SwMDWriter::SwMDWriter(const OUString& rBaseURL) { SetBaseURL(rBaseURL); } ErrCode SwMDWriter::WriteStream() { + Strm().SetStreamCharSet(RTL_TEXTENCODING_UTF8); if (m_bShowProgress) ::StartProgress(STR_STATSTR_W4WWRITE, 0, sal_Int32(m_pDoc->GetNodes().Count()), m_pDoc->GetDocShell()); @@ -400,7 +550,7 @@ void SwMDWriter::Out_SwDoc(SwPaM* pPam) if (!bFirstLine) m_pCurrentPam->GetPoint()->SetContent(0); - OutMarkdown_SwTextNode(*this, *pTextNd); + OutMarkdown_SwTextNode(*this, *pTextNd, bFirstLine); } } else if (rNd.IsTableNode())