sw/qa/uibase/shells/data/three-changes.fodt | 40 +++++++++++++ sw/qa/uibase/shells/shells.cxx | 84 ++++++++++++++++++++++++++++ sw/source/uibase/uno/loktxdoc.cxx | 77 +++++++++++++++++++++++++ 3 files changed, 201 insertions(+)
New commits: commit 8cff5914c4dde558a86f6bfd0f5c86b0d3325328 Author: Mike Kaganski <mike.kagan...@collabora.com> AuthorDate: Mon Jun 16 22:03:43 2025 +0500 Commit: Mike Kaganski <mike.kagan...@collabora.com> CommitDate: Tue Jun 17 18:17:42 2025 +0200 LOK Extract API: introduce redline data extraction Filter name is "trackchanges". Returned are "TrackChanges.ByIndex.N" numbered JSON nodes with these fields: * "type": string ("Delete", "Insert", "Format") * "datetime": string (ISO datetime stamp) * "author": string * "description": string * "comment": string * "text_before": string (up to 200 characters) * "text_after": string (up to 200 characters) Change-Id: I467bb9989d2eb461c74f50b53e039c1b2260e74c Reviewed-on: https://gerrit.libreoffice.org/c/core/+/186579 Reviewed-by: Miklos Vajna <vmik...@collabora.com> Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> Reviewed-on: https://gerrit.libreoffice.org/c/core/+/186608 Reviewed-by: Mike Kaganski <mike.kagan...@collabora.com> Tested-by: Jenkins diff --git a/sw/qa/uibase/shells/data/three-changes.fodt b/sw/qa/uibase/shells/data/three-changes.fodt new file mode 100644 index 000000000000..0c7072b244a0 --- /dev/null +++ b/sw/qa/uibase/shells/data/three-changes.fodt @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.4" office:mimetype="application/vnd.oasis.opendocument.text"> + <office:automatic-styles> + <style:style style:name="T1" style:family="text"> + <style:text-properties fo:font-weight="bold"/> + </style:style> + </office:automatic-styles> + <office:body> + <office:text> + <text:tracked-changes text:track-changes="false"> + <text:changed-region xml:id="ct2412392131792" text:id="ct2412392131792"> + <text:deletion> + <office:change-info> + <dc:creator>Mike</dc:creator> + <dc:date>2025-06-16T14:08:27</dc:date> + </office:change-info> + </text:deletion> + </text:changed-region> + <text:changed-region xml:id="ct2412428113776" text:id="ct2412428113776"> + <text:format-change> + <office:change-info> + <dc:creator>Mike</dc:creator> + <dc:date>2025-06-17T12:41:00</dc:date> + </office:change-info> + </text:format-change> + </text:changed-region> + <text:changed-region xml:id="ct2412428117232" text:id="ct2412428117232"> + <text:insertion> + <office:change-info> + <dc:creator>Mike</dc:creator> + <dc:date>2025-06-17T12:41:19</dc:date> + </office:change-info> + </text:insertion> + </text:changed-region> + </text:tracked-changes> + <text:p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum consequat mi quis pretium semper. Proin luctus orci ac neque venenatis, quis commodo dolor posuere. Curabitur dignissim sapien quis cursus egestas. <text:change-start text:change-id="ct2412392131792"/>Donec<text:change-end text:change-id="ct2412392131792"/> blandit auctor arcu, nec <text:change-start text:change-id="ct2412428113776"/><text:span text:style-name="T1">pellentesque</text:span><text:change-end text:change-id="ct2412428113776"/> eros molestie eget. In consectetur aliquam hendrerit. Sed cursus mauris vitae ligula pellentesque, non pellentesque urna aliquet. Fusce placerat mauris enim, nec rutrum purus semper vel. Praesent tincidunt neque eu pellentesque pharetra. Fusce pellentesque est orci.<text:change-start text:change-id="ct2412428117232"/> Sapienti sat.<text:change-end text:change-id="ct2412428117232"/></text:p> + </office:text> + </office:body> +</office:document> \ No newline at end of file diff --git a/sw/qa/uibase/shells/shells.cxx b/sw/qa/uibase/shells/shells.cxx index 24b7b92361c7..bb901f601020 100644 --- a/sw/qa/uibase/shells/shells.cxx +++ b/sw/qa/uibase/shells/shells.cxx @@ -47,6 +47,10 @@ #include <unotxdoc.hxx> #include <tools/json_writer.hxx> +#include <boost/property_tree/json_parser.hpp> + +using namespace std::string_literals; + /// Covers sw/source/uibase/shells/ fixes. class SwUibaseShellsTest : public SwModelTestBase { @@ -884,6 +888,86 @@ CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testDocumentStructureDocProperties) CPPUNIT_ASSERT_EQUAL(aExpectedStr, aJsonWriter.finishAndGetAsOString()); } +CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testDocumentStructureExtractRedlines) +{ + createSwDoc("three-changes.fodt"); + + // extract + tools::JsonWriter aJsonWriter; + std::string_view aCommand(".uno:ExtractDocumentStructure?filter=trackchanges"); + getSwTextDoc()->getCommandValues(aJsonWriter, aCommand); + + boost::property_tree::ptree tree; + std::stringstream aStream(std::string(aJsonWriter.finishAndGetAsOString())); + boost::property_tree::read_json(aStream, tree); + + CPPUNIT_ASSERT_EQUAL(size_t(1), tree.size()); + boost::property_tree::ptree docStructure = tree.get_child("DocStructure"); + CPPUNIT_ASSERT_EQUAL(size_t(3), docStructure.size()); + auto it = docStructure.begin(); + + { + // First change + CPPUNIT_ASSERT(it != docStructure.end()); + const auto & [ name, change ] = *it; + CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.0"s, name); + CPPUNIT_ASSERT_EQUAL(size_t(7), change.size()); + CPPUNIT_ASSERT_EQUAL("Delete"s, change.get<std::string>("type")); + CPPUNIT_ASSERT_EQUAL("2025-06-16T14:08:27"s, change.get<std::string>("datetime")); + CPPUNIT_ASSERT_EQUAL("Mike"s, change.get<std::string>("author")); + CPPUNIT_ASSERT_EQUAL("Delete “Donec”"s, change.get<std::string>("description")); + CPPUNIT_ASSERT_EQUAL(""s, change.get<std::string>("comment")); + auto text_before = change.get<std::string>("text-before"); + CPPUNIT_ASSERT_EQUAL(size_t(200), text_before.size()); + CPPUNIT_ASSERT(text_before.ends_with(" egestas. ")); + auto text_after = change.get<std::string>("text-after"); + CPPUNIT_ASSERT_EQUAL(size_t(200), text_after.size()); + CPPUNIT_ASSERT(text_after.starts_with(" blandit ")); + ++it; + } + + { + // Second change + CPPUNIT_ASSERT(it != docStructure.end()); + const auto & [ name, change ] = *it; + CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.1"s, name); + CPPUNIT_ASSERT_EQUAL(size_t(7), change.size()); + CPPUNIT_ASSERT_EQUAL("Format"s, change.get<std::string>("type")); + CPPUNIT_ASSERT_EQUAL("2025-06-17T12:41:00"s, change.get<std::string>("datetime")); + CPPUNIT_ASSERT_EQUAL("Mike"s, change.get<std::string>("author")); + CPPUNIT_ASSERT_EQUAL("Attributes changed"s, change.get<std::string>("description")); + CPPUNIT_ASSERT_EQUAL(""s, change.get<std::string>("comment")); + auto text_before = change.get<std::string>("text-before"); + CPPUNIT_ASSERT_EQUAL(size_t(200), text_before.size()); + CPPUNIT_ASSERT(text_before.ends_with(" arcu, nec ")); + auto text_after = change.get<std::string>("text-after"); + CPPUNIT_ASSERT_EQUAL(size_t(200), text_after.size()); + CPPUNIT_ASSERT(text_after.starts_with(" eros ")); + ++it; + } + + { + // Third change + CPPUNIT_ASSERT(it != docStructure.end()); + const auto & [ name, change ] = *it; + CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.2"s, name); + CPPUNIT_ASSERT_EQUAL(size_t(7), change.size()); + CPPUNIT_ASSERT_EQUAL("Insert"s, change.get<std::string>("type")); + CPPUNIT_ASSERT_EQUAL("2025-06-17T12:41:19"s, change.get<std::string>("datetime")); + CPPUNIT_ASSERT_EQUAL("Mike"s, change.get<std::string>("author")); + CPPUNIT_ASSERT_EQUAL("Insert “ Sapienti sat.”"s, change.get<std::string>("description")); + CPPUNIT_ASSERT_EQUAL(""s, change.get<std::string>("comment")); + auto text_before = change.get<std::string>("text-before"); + CPPUNIT_ASSERT_EQUAL(size_t(200), text_before.size()); + CPPUNIT_ASSERT(text_before.ends_with(" est orci.")); + auto text_after = change.get<std::string>("text-after"); + CPPUNIT_ASSERT(text_after.empty()); + ++it; + } + + CPPUNIT_ASSERT(bool(it == docStructure.end())); +} + CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testUpdateRefmarks) { // Given a document with two refmarks, one is not interesting the other is a citation: diff --git a/sw/source/uibase/uno/loktxdoc.cxx b/sw/source/uibase/uno/loktxdoc.cxx index 203a96bb2ce1..8c9613f3415e 100644 --- a/sw/source/uibase/uno/loktxdoc.cxx +++ b/sw/source/uibase/uno/loktxdoc.cxx @@ -39,6 +39,7 @@ #include <wrtsh.hxx> #include <txtrfmrk.hxx> #include <ndtxt.hxx> +#include <unoredlines.hxx> #include <unoport.hxx> #include <unoprnms.hxx> @@ -63,6 +64,37 @@ using namespace ::com::sun::star; namespace { +// A helper class to make it easier to put UNO property values to JSON with a given name. +// Removes noise from code. +class PropertyExtractor +{ +public: + PropertyExtractor(uno::Reference<beans::XPropertySet>& xProperties, tools::JsonWriter& rWriter) + : m_xProperties(xProperties) + , m_rWriter(rWriter) + { + } + + template <typename T> void extract(const OUString& unoName, std::string_view jsonName) + { + if (T val; m_xProperties->getPropertyValue(unoName) >>= val) + { + if constexpr (std::is_same_v<T, util::DateTime>) + { + OUStringBuffer buf(32); + sax::Converter::convertDateTime(buf, val, nullptr, true); + m_rWriter.put(jsonName, buf.makeStringAndClear()); + } + else + m_rWriter.put(jsonName, val); + } + } + +private: + uno::Reference<beans::XPropertySet>& m_xProperties; + tools::JsonWriter& m_rWriter; +}; + /// Implements getCommandValues(".uno:TextFormFields"). /// /// Parameters: @@ -822,6 +854,48 @@ void GetDocStructureDocProps(tools::JsonWriter& rJsonWriter, const SwDocShell* p } } +/// Implements getCommandValues(".uno:ExtractDocumentStructures") for redlines +void GetDocStructureTrackChanges(tools::JsonWriter& rJsonWriter, const SwDocShell* pDocShell) +{ + auto xRedlinesEnum = pDocShell->GetBaseModel()->getRedlines()->createEnumeration(); + for (sal_Int32 i = 0; xRedlinesEnum->hasMoreElements(); ++i) + { + auto xRedlineProperties = xRedlinesEnum->nextElement().query<beans::XPropertySet>(); + assert(xRedlineProperties); + + auto TrackChangesNode + = rJsonWriter.startNode(Concat2View("TrackChanges.ByIndex." + OString::number(i))); + + PropertyExtractor extractor{ xRedlineProperties, rJsonWriter }; + + extractor.extract<OUString>(UNO_NAME_REDLINE_TYPE, "type"); + extractor.extract<css::util::DateTime>(UNO_NAME_REDLINE_DATE_TIME, "datetime"); + extractor.extract<OUString>(UNO_NAME_REDLINE_AUTHOR, "author"); + extractor.extract<OUString>(UNO_NAME_REDLINE_DESCRIPTION, "description"); + extractor.extract<OUString>(UNO_NAME_REDLINE_COMMENT, "comment"); + if (auto xStart = xRedlineProperties->getPropertyValue(UNO_NAME_REDLINE_START) + .query<css::text::XTextRange>()) + { + auto xCursor = xStart->getText()->createTextCursorByRange(xStart); + xCursor->goLeft(200, /*bExpand*/ true); + rJsonWriter.put("text-before", xCursor->getString()); + } + if (auto xEnd = xRedlineProperties->getPropertyValue(UNO_NAME_REDLINE_END) + .query<css::text::XTextRange>()) + { + auto xCursor = xEnd->getText()->createTextCursorByRange(xEnd); + xCursor->goRight(200, /*bExpand*/ true); + rJsonWriter.put("text-after", xCursor->getString()); + } + // UNO_NAME_REDLINE_IDENTIFIER: OUString (the value of a pointer, not persistent) + // UNO_NAME_REDLINE_MOVED_ID: sal_uInt32; 0 == not moved, 1 == moved, but don't have its pair, 2+ == unique ID + // UNO_NAME_REDLINE_SUCCESSOR_DATA: uno::Sequence<beans::PropertyValue> + // UNO_NAME_IS_IN_HEADER_FOOTER: bool + // UNO_NAME_MERGE_LAST_PARA: bool + // UNO_NAME_REDLINE_TEXT: uno::Reference<text::XText> + } +} + /// Implements getCommandValues(".uno:ExtractDocumentStructures"). /// /// Parameters: @@ -844,6 +918,9 @@ void GetDocStructure(tools::JsonWriter& rJsonWriter, const SwDocShell* pDocShell if (filter.isEmpty() || filter == "docprops") GetDocStructureDocProps(rJsonWriter, pDocShell); + + if (filter.isEmpty() || filter == "trackchanges") + GetDocStructureTrackChanges(rJsonWriter, pDocShell); } /// Implements getCommandValues(".uno:Sections").