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 f136069168d321d5ce3c0725ca85435ee7d377ac Author: Mike Kaganski <mike.kagan...@collabora.com> AuthorDate: Mon Jun 16 22:03:43 2025 +0500 Commit: Miklos Vajna <vmik...@collabora.com> CommitDate: Tue Jun 17 13:35:02 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> 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 999a1aa54109..39571984abf9 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 2bd36c3dbc58..ca3e5ab49b44 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").