Repository navigation
Expand file tree
/
Copy pathrich_html.py
More file actions
299 lines (264 loc) · 13.3 KB
/
Copy pathrich_html.py
File metadata and controls
299 lines (264 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# WriterAgent - AI Writing Assistant for LibreOffice
# Copyright (c) 2024 John Balis
# Copyright (c) 2026 KeithCu (modifications and relicensing)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Import HTML into a Calc cell via a hidden Writer document and transferable paste."""
from __future__ import annotations
import html as html_std
import logging
import sys
from typing import Any
from plugin.framework.errors import ToolExecutionError
from plugin.framework.uno_context import clear_writer_body, get_desktop
from plugin.calc.address_utils import parse_address
from plugin.calc.bridge import CalcBridge
import plugin.writer.format as format_support
log = logging.getLogger("writeragent.calc")
# Not "_blank" and not "_default". testing_runner's Windows keeper loads
# Hidden Writer on "_blank" (542 used the same target and 33699746211 still
# hung). Linux UNO probe: a second "_blank" gets a new RuntimeUID (not
# document reuse) but Hidden frame names stay empty, so a Windows
# frame-manager collision on the shared "_blank" target is still the
# remaining hunch. CREATE|GLOBAL = FrameSearchFlag 8|55 — named target
# with flags 0 can search instead of creating.
_HTML_WRITER_TARGET = "_wa_calc_html"
_HTML_WRITER_SEARCH_FLAGS = 8 | 55
def _step(msg: str) -> None:
"""log.info plus stderr flush so GHA names the last call on a hang."""
log.info(msg)
print(msg, file=sys.stderr, flush=True)
def _writer_runtime_uid(doc: Any) -> str:
"""Best-effort RuntimeUID so we can say keeper vs new frame without changing load."""
try:
uid = getattr(doc, "RuntimeUID", None)
if uid:
return str(uid)
except Exception:
pass
return "-"
def _desktop_writer_uids(desktop: Any) -> list[str]:
"""RuntimeUIDs of open Writer docs. Read-only; must not load or close anything.
Safe only **before** loading/closing the temp Writer on the insert path
(``writers_open=`` logging). On Windows, ``desktop.getComponents()`` can
block indefinitely **after** ``temp_doc.close()`` when Calc still holds a
live ``insertTransferable`` paste — GHA 33771766524 / 33772063173: the
in-function ``writers_after_close`` call returned in ~10ms, then teardown
``getComponents`` ~450ms later hung forever. Do not call this after closing
a post-paste Writer; ``insert_cell_html_rich`` reuses ``_wa_calc_html``
instead of closing on success.
"""
uids: list[str] = []
try:
enum = desktop.getComponents().createEnumeration()
except Exception:
return uids
# ``is True``: MagicMock.hasMoreElements() is truthy and would loop.
n = 0
while n < 64:
try:
if enum.hasMoreElements() is not True:
break
component = enum.nextElement()
except Exception:
break
n += 1
try:
if component.supportsService("com.sun.star.text.TextDocument"):
uids.append(_writer_runtime_uid(component))
except Exception:
continue
return uids
def _controller_get_transferable(controller: Any) -> Any:
"""Return transferable for the current selection (Writer controller)."""
if controller is None:
raise ToolExecutionError("Controller is None")
t = getattr(controller, "getTransferable", None)
if callable(t):
return t()
try:
from com.sun.star.datatransfer import XTransferableSupplier
xs = controller.queryInterface(XTransferableSupplier)
if xs is not None:
return xs.getTransferable()
except Exception:
log.debug("getTransferable via queryInterface failed", exc_info=True)
raise ToolExecutionError("Writer controller does not support getTransferable; cannot paste HTML into cell.")
def _controller_insert_transferable(controller: Any, transferable: Any) -> None:
if controller is None:
raise ToolExecutionError("Calc controller is None")
ins = getattr(controller, "insertTransferable", None)
if not callable(ins):
raise ToolExecutionError("Calc controller does not support insertTransferable.")
ins(transferable)
def _service_manager(uno_ctx: Any) -> Any:
"""ServiceManager from a UNO context. Best-effort; never raises."""
getter = getattr(uno_ctx, "getServiceManager", None)
if callable(getter):
try:
smgr = getter()
if smgr is not None:
return smgr
except Exception:
pass
return getattr(uno_ctx, "ServiceManager", None)
def _system_clipboard(uno_ctx: Any) -> Any:
"""SystemClipboard or None. Must not import chatbot (LibrePy ships this module)."""
smgr = _service_manager(uno_ctx)
if smgr is None:
return None
try:
return smgr.createInstanceWithContext("com.sun.star.datatransfer.clipboard.SystemClipboard", uno_ctx)
except Exception:
return None
def _release_clipboard_if_holds(uno_ctx: Any, transferable: Any) -> str:
"""If SystemClipboard still owns *transferable*, replace it before closing the Writer.
SwXTextView.getTransferable() builds a SwTransferable bound to that Writer
shell (PrepareForCopy). On Windows the OLE clipboard thread can keep that
transferable as owner after insertTransferable. Only touch the clipboard when
it actually holds *our* transferable so a normal paste does not clobber an
unrelated user copy. GHA 33771766524: insertTransferable often leaves
SystemClipboard empty (release returns ``empty``); the hang was closing the
source Writer while Calc still held the paste — see ``insert_cell_html_rich``.
"""
try:
clip = _system_clipboard(uno_ctx)
if clip is None:
return "no_clipboard"
contents = clip.getContents()
if contents is None:
return "empty"
same = contents is transferable
try:
equal = contents == transferable
except Exception:
equal = False
if not (same or equal):
return "not_ours contents_id=%s xfer_id=%s" % (id(contents), id(transferable))
# None, None drops the previous owner (lostOwnership) without a new OLE source.
clip.setContents(None, None)
return "released contents_id=%s xfer_id=%s" % (id(contents), id(transferable))
except Exception as exc:
return "error:%s" % exc
def insert_cell_html_rich(doc: Any, uno_ctx: Any, cell_address: str, html: str, *, config_svc: Any = None) -> None:
"""Replace one cell's text with rich content parsed from *html* (active sheet).
*uno_ctx* is the UNO component context (e.g. ``ToolContext.ctx``).
Imports HTML using the same StarWriter HTML filter as Writer, then pastes
into the target cell. Images and embedded objects are not supported.
"""
if not (html or "").strip():
raise ToolExecutionError("HTML content is empty")
bridge = CalcBridge(doc)
col, row = parse_address(cell_address.strip())
sheet = bridge.get_active_sheet()
cell = bridge.get_cell(sheet, col, row)
content = html_std.unescape(html)
prepared = format_support._ensure_html_linebreaks(content)
temp_doc = None
close_temp = True
transferable: Any = None
pasted = False
try:
desktop = get_desktop(uno_ctx)
hidden = format_support.create_property_value("Hidden", True)
# Do not import chatbot.create_hidden_html_writer — this module ships
# in LibrePy without chatbot.
_step("insert_cell_html_rich: loadComponentFromURL start target=%s hidden=True flags=%s" % (_HTML_WRITER_TARGET, _HTML_WRITER_SEARCH_FLAGS))
writers_before = _desktop_writer_uids(desktop)
_step("insert_cell_html_rich: writers_open=%s uids=%s" % (len(writers_before), writers_before))
temp_doc = desktop.loadComponentFromURL("private:factory/swriter", _HTML_WRITER_TARGET, _HTML_WRITER_SEARCH_FLAGS, (hidden,))
temp_uid = _writer_runtime_uid(temp_doc) if temp_doc is not None else "-"
reused_existing = bool(temp_uid != "-" and temp_uid in writers_before)
# Never close the Windows keeper. After a successful paste we also
# keep this Writer (see finally) — CREATE|GLOBAL reuses _wa_calc_html.
close_temp = not reused_existing
_step("insert_cell_html_rich: loadComponentFromURL done target=%s temp_uid=%s reused_existing=%s close_temp=%s" % (_HTML_WRITER_TARGET, temp_uid, reused_existing, close_temp))
if temp_doc is None or not hasattr(temp_doc, "getText"):
raise ToolExecutionError("Could not create temporary Writer document")
text = temp_doc.getText()
# What was wrong: setString("") leaves a default-template letterhead that is
# only a table, text frame, or drawing (body string is already ""). How it
# happened: private:factory/swriter honours that template, and a successful
# paste keeps this Writer (CREATE|GLOBAL reuses _wa_calc_html), so the first
# rich-HTML paste copies the letterhead into the cell and later pastes append
# onto the previous fragment. Why this fixes it: clear_writer_body disposes
# those objects and then clears the text, on a fresh load and on the reused
# keeper, before the HTML is inserted.
clear_writer_body(temp_doc)
cursor = text.createTextCursor()
cursor.gotoStart(False)
_step("insert_cell_html_rich: HTML insert start")
format_support._insert_starwriter_html_at_cursor(temp_doc, cursor, prepared, config_svc=config_svc)
_step("insert_cell_html_rich: HTML insert done")
# Hidden Writer docs must not use getViewCursor() — it can crash the
# process (no real view). Select the whole body with a text cursor and
# XSelectionSupplier.select, then getTransferable().
w_ctrl = temp_doc.getCurrentController()
body = temp_doc.getText()
sel = body.createTextCursor()
sel.gotoStart(False)
sel.gotoEnd(True)
w_ctrl.select(sel)
_step("insert_cell_html_rich: getTransferable start")
transferable = _controller_get_transferable(w_ctrl)
# Do not snapshot SystemClipboard here. GHA 33771766524: getContents()
# was empty on every call, but those queries still attach the Windows
# OLE clipboard thread (CMtaOleClipboard) and the next desktop enum
# then blocked. Release-if-ours below is the only clipboard touch.
_step("insert_cell_html_rich: getTransferable done xfer_id=%s" % id(transferable))
cell.getText().setString("")
c_ctrl = doc.getCurrentController()
_step("insert_cell_html_rich: select cell start")
c_ctrl.select(cell)
_step("insert_cell_html_rich: select cell done")
_step("insert_cell_html_rich: insertTransferable start")
_controller_insert_transferable(c_ctrl, transferable)
_step("insert_cell_html_rich: insertTransferable done")
# SwXTextView.getTransferable builds a SwTransferable bound to this
# Writer (PrepareForCopy). Keep the #572 rule: if SystemClipboard
# still owns that object, drop it before any close. Do not gc.collect()
# — GHA 33771769928 aborted in freeUnoInterfaceProxy → SdrObject dtor
# → SfxItemPool::unregisterItemSet after #572 forced a full GC.
_step("insert_cell_html_rich: clipboard release start")
released = _release_clipboard_if_holds(uno_ctx, transferable)
transferable = None
pasted = True
_step("insert_cell_html_rich: clipboard release done released=%s" % released)
except ToolExecutionError:
raise
except Exception as e:
log.debug("insert_cell_html_rich failed", exc_info=True)
raise ToolExecutionError(f"Failed to insert HTML into cell: {e}") from e
finally:
if transferable is not None:
try:
_release_clipboard_if_holds(uno_ctx, transferable)
except Exception:
pass
transferable = None
# Windows (GHA 33771766524, 33772063173): after a live paste,
# temp_doc.close() returns and an immediate writers_after_close enum
# can still return — but ~450ms later desktop.getComponents() blocks
# forever while Calc holds the paste. Do not close on success; reuse
# _wa_calc_html (CREATE|GLOBAL) and clear the body on the next call.
# Keep #572: release SystemClipboard only when it owns our transferable.
if temp_doc is not None and close_temp and not pasted:
try:
_step("insert_cell_html_rich: close start")
temp_doc.close(True)
_step("insert_cell_html_rich: close done")
except Exception:
log.debug("temp Writer close failed", exc_info=True)
elif temp_doc is not None:
_step("insert_cell_html_rich: close skipped pasted=%s reused_existing=%s" % (pasted, not close_temp))