1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 #include "XMLTextMarkImportContext.hxx"
23 #include <rtl/ustring.hxx>
24 #include <sal/log.hxx>
25 #include <osl/diagnose.h>
26 #include <sax/tools/converter.hxx>
27 #include <xmloff/xmluconv.hxx>
28 #include <xmloff/xmltoken.hxx>
29 #include <xmloff/xmlimp.hxx>
30 #include <xmloff/xmlnamespace.hxx>
31 #include <xmloff/odffields.hxx>
32 #include <xmloff/xmlement.hxx>
33 #include <com/sun/star/frame/XModel.hpp>
34 #include <com/sun/star/text/ControlCharacter.hpp>
35 #include <com/sun/star/text/XTextContent.hpp>
36 #include <com/sun/star/text/XTextRangeCompare.hpp>
37 #include <com/sun/star/beans/XPropertySet.hpp>
38 #include <com/sun/star/lang/XMultiServiceFactory.hpp>
39 #include <com/sun/star/container/XNamed.hpp>
40 #include <com/sun/star/rdf/XMetadatable.hpp>
42 #include <com/sun/star/text/XFormField.hpp>
43 #include <comphelper/diagnose_ex.hxx>
45 #include <RDFaImportHelper.hxx>
48 using namespace ::com::sun::star
;
49 using namespace ::com::sun::star::text
;
50 using namespace ::com::sun::star::uno
;
51 using namespace ::com::sun::star::beans
;
52 using namespace ::com::sun::star::lang
;
53 using namespace ::com::sun::star::container
;
54 using namespace ::com::sun::star::xml::sax
;
55 using namespace ::xmloff::token
;
58 XMLFieldParamImportContext::XMLFieldParamImportContext(
60 XMLTextImportHelper
& rHlp
) :
61 SvXMLImportContext(rImport
),
67 void XMLFieldParamImportContext::startFastElement(sal_Int32
/*nElement*/, const css::uno::Reference
< css::xml::sax::XFastAttributeList
> & xAttrList
)
72 for (auto &aIter
: sax_fastparser::castToFastAttributeList( xAttrList
))
74 switch (aIter
.getToken())
76 case XML_ELEMENT(FIELD
, XML_NAME
):
77 sName
= aIter
.toString();
79 case XML_ELEMENT(FIELD
, XML_VALUE
):
80 sValue
= aIter
.toString();
83 XMLOFF_WARN_UNKNOWN("xmloff", aIter
);
86 if (rHelper
.hasCurrentFieldCtx() && !sName
.isEmpty()) {
87 rHelper
.addFieldParam(sName
, sValue
);
92 XMLTextMarkImportContext::XMLTextMarkImportContext(
94 XMLTextImportHelper
& rHlp
,
95 uno::Reference
<uno::XInterface
> & io_rxCrossRefHeadingBookmark
)
96 : SvXMLImportContext(rImport
)
98 , m_rxCrossRefHeadingBookmark(io_rxCrossRefHeadingBookmark
)
100 , m_bHaveAbout(false)
106 enum lcl_MarkType
{ TypeReference
, TypeReferenceStart
, TypeReferenceEnd
,
107 TypeBookmark
, TypeBookmarkStart
, TypeBookmarkEnd
,
108 TypeFieldmark
, TypeFieldmarkStart
, TypeFieldmarkSeparator
, TypeFieldmarkEnd
113 SvXMLEnumMapEntry
<lcl_MarkType
> const lcl_aMarkTypeMap
[] =
115 { XML_REFERENCE_MARK
, TypeReference
},
116 { XML_REFERENCE_MARK_START
, TypeReferenceStart
},
117 { XML_REFERENCE_MARK_END
, TypeReferenceEnd
},
118 { XML_BOOKMARK
, TypeBookmark
},
119 { XML_BOOKMARK_START
, TypeBookmarkStart
},
120 { XML_BOOKMARK_END
, TypeBookmarkEnd
},
121 { XML_FIELDMARK
, TypeFieldmark
},
122 { XML_FIELDMARK_START
, TypeFieldmarkStart
},
123 { XML_FIELDMARK_SEPARATOR
, TypeFieldmarkSeparator
},
124 { XML_FIELDMARK_END
, TypeFieldmarkEnd
},
125 { XML_TOKEN_INVALID
, lcl_MarkType(0) },
129 static const OUString
& lcl_getFormFieldmarkName(std::u16string_view name
)
131 if (name
== ODF_FORMCHECKBOX
||
132 name
== u
"msoffice.field.FORMCHECKBOX" ||
133 name
== u
"ecma.office-open-xml.field.FORMCHECKBOX")
134 return ODF_FORMCHECKBOX
;
135 else if (name
== ODF_FORMDROPDOWN
||
136 name
== u
"ecma.office-open-xml.field.FORMDROPDOWN")
137 return ODF_FORMDROPDOWN
;
139 return EMPTY_OUSTRING
;
142 static OUString
lcl_getFieldmarkName(OUString
const& name
)
144 if (name
== "msoffice.field.FORMTEXT" ||
145 name
== "ecma.office-open-xml.field.FORMTEXT")
152 void XMLTextMarkImportContext::startFastElement( sal_Int32 nElement
,
153 const css::uno::Reference
< css::xml::sax::XFastAttributeList
>& xAttrList
)
155 if (!FindName(xAttrList
))
157 m_sBookmarkName
.clear();
160 if ((nElement
& TOKEN_MASK
) == XML_FIELDMARK_START
||
161 (nElement
& TOKEN_MASK
) == XML_FIELDMARK
)
163 if (m_sBookmarkName
.isEmpty())
165 m_sBookmarkName
= "Unknown";
167 m_rHelper
.pushFieldCtx( m_sBookmarkName
, m_sFieldName
);
170 if ((nElement
& TOKEN_MASK
) == XML_BOOKMARK_START
)
172 m_rHelper
.setBookmarkAttributes(m_sBookmarkName
, m_isHidden
, m_sCondition
);
176 static auto InsertFieldmark(SvXMLImport
& rImport
,
177 XMLTextImportHelper
& rHelper
, bool const isFieldmarkSeparatorMissing
) -> void
179 assert(rHelper
.hasCurrentFieldCtx()); // was set up in StartElement()
181 // fdo#86795 check if it's actually a checkbox first
182 auto const [ name
, type
] = rHelper
.getCurrentFieldType();
183 OUString
const fieldmarkTypeName
= lcl_getFieldmarkName(type
);
184 if (fieldmarkTypeName
== ODF_FORMCHECKBOX
||
185 fieldmarkTypeName
== ODF_FORMDROPDOWN
)
186 { // sw can't handle checkbox with start+end
187 SAL_INFO("xmloff.text", "invalid fieldmark-start/fieldmark-end ignored");
191 uno::Reference
<text::XTextRange
> const xStartRange(rHelper
.getCurrentFieldStart());
192 uno::Reference
<text::XTextCursor
> const xCursor(
193 rHelper
.GetText()->createTextCursorByRange(xStartRange
));
194 uno::Reference
<text::XTextRangeCompare
> const xCompare(rHelper
.GetText(), uno::UNO_QUERY
);
195 if (xCompare
->compareRegionStarts(xStartRange
, rHelper
.GetCursorAsRange()) < 0)
197 SAL_WARN("xmloff.text", "invalid field mark positions");
200 xCursor
->gotoRange(rHelper
.GetCursorAsRange(), true);
202 Reference
<XTextContent
> const xContent
= XMLTextMarkImportContext::CreateAndInsertMark(
203 rImport
, u
"com.sun.star.text.Fieldmark"_ustr
, name
, xCursor
,
204 OUString(), isFieldmarkSeparatorMissing
);
209 // setup fieldmark...
210 Reference
<text::XFormField
> const xFormField(xContent
, UNO_QUERY
);
211 assert(xFormField
.is());
213 xFormField
->setFieldType(fieldmarkTypeName
);
214 } catch (uno::RuntimeException
const&) {
215 // tdf#140437 somehow old documents had the field code in the type
216 // attribute instead of field:param
217 SAL_INFO("xmloff.text", "invalid fieldmark type, converting to param");
218 // add without checking: FieldParamImporter::Import() catches ElementExistException
219 rHelper
.addFieldParam(ODF_CODE_PARAM
, fieldmarkTypeName
);
220 xFormField
->setFieldType(ODF_UNHANDLED
);
222 rHelper
.setCurrentFieldParamsTo(xFormField
);
223 // move cursor after setFieldType as that may delete/re-insert
224 rHelper
.GetCursor()->gotoRange(xContent
->getAnchor()->getEnd(), false);
225 rHelper
.GetCursor()->goLeft(1, false); // move before CH_TXT_ATR_FIELDEND
226 // tdf#129520: AppendTextNode() ignores the content index!
227 // plan B: insert a spurious paragraph break now and join
228 // it in PopFieldmark()!
229 rHelper
.GetText()->insertControlCharacter(rHelper
.GetCursor(),
230 text::ControlCharacter::PARAGRAPH_BREAK
, false);
231 rHelper
.GetCursor()->goLeft(1, false); // back to previous paragraph
234 static auto PopFieldmark(XMLTextImportHelper
& rHelper
) -> void
236 // can't verify name because it's not written as an attribute...
237 uno::Reference
<text::XTextContent
> const xField(rHelper
.popFieldCtx(),
242 if (rHelper
.GetText() == xField
->getAnchor()->getText())
245 { // skip CH_TXT_ATR_FIELDEND
246 rHelper
.GetCursor()->goRight(1, true);
247 rHelper
.GetCursor()->setString(OUString()); // undo AppendTextNode from InsertFieldmark
248 rHelper
.GetCursor()->gotoRange(xField
->getAnchor()->getEnd(), false);
250 catch (uno::Exception
const&)
252 assert(false); // must succeed
257 SAL_INFO("xmloff.text", "fieldmark has invalid positions");
258 // could either dispose it or leave it to end at the end of the document?
263 void XMLTextMarkImportContext::endFastElement(sal_Int32 nElement
)
265 static constexpr OUString sAPI_bookmark
= u
"com.sun.star.text.Bookmark"_ustr
;
268 if (!SvXMLUnitConverter::convertEnum(nTmp
, SvXMLImport::getNameFromToken(nElement
), lcl_aMarkTypeMap
))
271 if (m_sBookmarkName
.isEmpty() && TypeFieldmarkEnd
!= nTmp
&& TypeFieldmarkSeparator
!= nTmp
)
277 // export point reference mark
278 CreateAndInsertMark(GetImport(),
279 u
"com.sun.star.text.ReferenceMark"_ustr
,
281 m_rHelper
.GetCursorAsRange()->getStart());
286 // tdf#94804: detect duplicate heading cross reference bookmarks
287 if (m_sBookmarkName
.startsWith("__RefHeading__"))
289 if (m_rxCrossRefHeadingBookmark
.is())
291 uno::Reference
<container::XNamed
> const xNamed(
292 m_rxCrossRefHeadingBookmark
, uno::UNO_QUERY
);
293 m_rHelper
.AddCrossRefHeadingMapping(
294 m_sBookmarkName
, xNamed
->getName());
295 break; // don't insert
302 const OUString formFieldmarkName
=lcl_getFormFieldmarkName(m_sFieldName
);
303 bool bImportAsField
= (nTmp
==TypeFieldmark
&& !formFieldmarkName
.isEmpty()); //@TODO handle abbreviation cases...
304 // export point bookmark
305 const Reference
<XInterface
> xContent(
306 CreateAndInsertMark(GetImport(),
307 (bImportAsField
? u
"com.sun.star.text.FormFieldmark"_ustr
: sAPI_bookmark
),
309 m_rHelper
.GetCursorAsRange()->getStart(),
311 if (nTmp
==TypeFieldmark
) {
312 if (xContent
.is() && bImportAsField
) {
313 // setup fieldmark...
314 Reference
< css::text::XFormField
> xFormField(xContent
, UNO_QUERY
);
315 xFormField
->setFieldType(formFieldmarkName
);
316 if (xFormField
.is() && m_rHelper
.hasCurrentFieldCtx()) {
317 m_rHelper
.setCurrentFieldParamsTo(xFormField
);
320 m_rHelper
.popFieldCtx();
322 if (TypeBookmark
== nTmp
323 && m_sBookmarkName
.startsWith("__RefHeading__"))
325 assert(xContent
.is());
326 m_rxCrossRefHeadingBookmark
= xContent
;
331 case TypeBookmarkStart
:
332 // save XTextRange for later construction of bookmark
334 std::shared_ptr
< ::xmloff::ParsedRDFaAttributes
>
336 if (m_bHaveAbout
&& TypeBookmarkStart
== nTmp
)
339 GetImport().GetRDFaImportHelper().ParseRDFa(
340 m_sAbout
, m_sProperty
,
341 m_sContent
, m_sDatatype
);
343 m_rHelper
.InsertBookmarkStartRange(
345 m_rHelper
.GetCursorAsRange()->getStart(),
346 m_sXmlId
, xRDFaAttributes
);
350 case TypeBookmarkEnd
:
352 // tdf#94804: detect duplicate heading cross reference bookmarks
353 if (m_sBookmarkName
.startsWith("__RefHeading__"))
355 if (m_rxCrossRefHeadingBookmark
.is())
357 uno::Reference
<container::XNamed
> const xNamed(
358 m_rxCrossRefHeadingBookmark
, uno::UNO_QUERY
);
359 m_rHelper
.AddCrossRefHeadingMapping(
360 m_sBookmarkName
, xNamed
->getName());
361 break; // don't insert
365 // get old range, and construct
366 Reference
<XTextRange
> xStartRange
;
367 std::shared_ptr
< ::xmloff::ParsedRDFaAttributes
>
369 if (m_rHelper
.FindAndRemoveBookmarkStartRange(
370 m_sBookmarkName
, xStartRange
,
371 m_sXmlId
, xRDFaAttributes
))
373 Reference
<XTextRange
> xEndRange(
374 m_rHelper
.GetCursorAsRange()->getStart());
376 // check if beginning and end are in same XText
377 if (xStartRange
.is() && xEndRange
.is() && xStartRange
->getText() == xEndRange
->getText())
379 // create range for insertion
380 Reference
<XTextCursor
> xInsertionCursor
=
381 m_rHelper
.GetText()->createTextCursorByRange(
384 xInsertionCursor
->gotoRange(xStartRange
, true);
385 } catch (uno::Exception
&) {
386 TOOLS_WARN_EXCEPTION("xmloff.text",
387 "cannot go to end position of bookmark");
390 //DBG_ASSERT(! xInsertionCursor->isCollapsed(),
391 // "we want no point mark");
392 // can't assert, because someone could
393 // create a file with subsequence
394 // start/end elements
396 Reference
<XInterface
> xContent
;
398 xContent
= CreateAndInsertMark(GetImport(),
405 const Reference
<rdf::XMetadatable
>
406 xMeta(xContent
, UNO_QUERY
);
407 GetImport().GetRDFaImportHelper().AddRDFa(
408 xMeta
, xRDFaAttributes
);
410 const Reference
<XPropertySet
> xPropertySet(xContent
, UNO_QUERY
);
411 if (xPropertySet
.is())
413 xPropertySet
->setPropertyValue(u
"BookmarkHidden"_ustr
, uno::Any(m_rHelper
.getBookmarkHidden(m_sBookmarkName
)));
414 xPropertySet
->setPropertyValue(u
"BookmarkCondition"_ustr
, uno::Any(m_rHelper
.getBookmarkCondition(m_sBookmarkName
)));
416 if (m_sBookmarkName
.startsWith("__RefHeading__"))
418 assert(xContent
.is());
419 m_rxCrossRefHeadingBookmark
= std::move(xContent
);
422 // else: beginning/end in different XText -> ignore!
424 // else: no start found -> ignore!
427 case TypeFieldmarkStart
:
431 case TypeFieldmarkSeparator
:
433 InsertFieldmark(GetImport(), m_rHelper
, false);
436 case TypeFieldmarkEnd
:
438 if (m_rHelper
.hasCurrentFieldCtx() && !m_rHelper
.hasCurrentFieldSeparator())
439 { // backward compat for old files without separator
440 InsertFieldmark(GetImport(), m_rHelper
, true);
442 PopFieldmark(m_rHelper
);
445 case TypeReferenceStart
:
446 case TypeReferenceEnd
:
447 OSL_FAIL("reference start/end are handled in txtparai !");
451 OSL_FAIL("unknown mark type");
456 css::uno::Reference
< css::xml::sax::XFastContextHandler
> XMLTextMarkImportContext::createFastChildContext(
458 const css::uno::Reference
< css::xml::sax::XFastAttributeList
>& )
460 return new XMLFieldParamImportContext(GetImport(), m_rHelper
);
464 Reference
<XTextContent
> XMLTextMarkImportContext::CreateAndInsertMark(
465 SvXMLImport
& rImport
,
466 const OUString
& sServiceName
,
467 const OUString
& sMarkName
,
468 const Reference
<XTextRange
> & rRange
,
469 const OUString
& i_rXmlId
,
470 bool const isFieldmarkSeparatorMissing
)
473 const Reference
<XMultiServiceFactory
> xFactory(rImport
.GetModel(),
475 Reference
<XInterface
> xIfc
;
479 xIfc
= xFactory
->createInstance(sServiceName
);
483 OSL_FAIL("CreateAndInsertMark: cannot create service?");
487 // set name (unless there is no name (text:meta))
488 const Reference
<XNamed
> xNamed(xIfc
, UNO_QUERY
);
491 xNamed
->setName(sMarkName
);
495 if (!sMarkName
.isEmpty())
497 OSL_FAIL("name given, but XNamed not supported?");
502 if (isFieldmarkSeparatorMissing
)
504 uno::Reference
<beans::XPropertySet
> const xProps(xIfc
, uno::UNO_QUERY_THROW
);
505 xProps
->setPropertyValue(u
"PrivateSeparatorAtStart"_ustr
, uno::Any(true));
508 // cast to XTextContent and attach to document
509 const Reference
<XTextContent
> xTextContent(xIfc
, UNO_QUERY
);
510 if (xTextContent
.is())
514 // if inserting marks, bAbsorb==sal_False will cause
515 // collapsing of the given XTextRange.
516 rImport
.GetTextImport()->GetText()->insertTextContent(rRange
,
519 // xml:id for RDF metadata -- after insertion!
520 rImport
.SetXmlId(xIfc
, i_rXmlId
);
524 catch (css::lang::IllegalArgumentException
&)
526 OSL_FAIL("CreateAndInsertMark: cannot insert?");
534 bool XMLTextMarkImportContext::FindName(
535 const Reference
<XFastAttributeList
> & xAttrList
)
537 bool bNameOK
= false;
539 // find name attribute first
540 for( auto& aIter
: sax_fastparser::castToFastAttributeList(xAttrList
) )
542 OUString sValue
= aIter
.toString();
543 switch(aIter
.getToken())
545 case XML_ELEMENT(TEXT
, XML_NAME
):
546 m_sBookmarkName
= sValue
;
549 case XML_ELEMENT(XML
, XML_ID
):
553 case XML_ELEMENT(XHTML
, XML_ABOUT
):
557 case XML_ELEMENT(XHTML
, XML_PROPERTY
):
558 m_sProperty
= sValue
;
560 case XML_ELEMENT(XHTML
, XML_CONTENT
):
563 case XML_ELEMENT(XHTML
, XML_DATATYPE
):
564 m_sDatatype
= sValue
;
566 case XML_ELEMENT(FIELD
, XML_TYPE
):
567 m_sFieldName
= sValue
;
569 case XML_ELEMENT(LO_EXT
, XML_HIDDEN
):
570 ::sax::Converter::convertBool(m_isHidden
, sValue
);
572 case XML_ELEMENT(LO_EXT
, XML_CONDITION
):
573 m_sCondition
= sValue
;
576 XMLOFF_WARN_UNKNOWN("xmloff", aIter
);
583 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */