summaryrefslogtreecommitdiffstats
path: root/sw/source/filter/ascii
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 05:54:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 05:54:39 +0000
commit267c6f2ac71f92999e969232431ba04678e7437e (patch)
tree358c9467650e1d0a1d7227a21dac2e3d08b622b2 /sw/source/filter/ascii
parentInitial commit. (diff)
downloadlibreoffice-267c6f2ac71f92999e969232431ba04678e7437e.tar.xz
libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.zip
Adding upstream version 4:24.2.0.upstream/4%24.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sw/source/filter/ascii')
-rw-r--r--sw/source/filter/ascii/ascatr.cxx388
-rw-r--r--sw/source/filter/ascii/parasc.cxx521
-rw-r--r--sw/source/filter/ascii/wrtasc.cxx230
-rw-r--r--sw/source/filter/ascii/wrtasc.hxx45
4 files changed, 1184 insertions, 0 deletions
diff --git a/sw/source/filter/ascii/ascatr.cxx b/sw/source/filter/ascii/ascatr.cxx
new file mode 100644
index 0000000000..c4f4d12490
--- /dev/null
+++ b/sw/source/filter/ascii/ascatr.cxx
@@ -0,0 +1,388 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright
+ * ownership. The ASF licenses this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#include <hintids.hxx>
+#include <tools/stream.hxx>
+#include <comphelper/string.hxx>
+#include <pam.hxx>
+#include <doc.hxx>
+#include <ndtxt.hxx>
+#include <IDocumentRedlineAccess.hxx>
+#include <redline.hxx>
+#include "wrtasc.hxx"
+#include <txatbase.hxx>
+#include <txtfld.hxx>
+#include <fmtftn.hxx>
+#include <fmtfld.hxx>
+#include <fldbas.hxx>
+#include <ftninfo.hxx>
+#include <numrule.hxx>
+
+#include <algorithm>
+
+/*
+ * This file contains all output functions of the ASCII-Writer;
+ * For all nodes, attributes, formats and chars.
+ */
+
+namespace {
+
+class SwASC_AttrIter
+{
+ SwASCWriter& m_rWrt;
+ const SwTextNode& m_rNd;
+ sal_Int32 m_nCurrentSwPos;
+
+ sal_Int32 SearchNext( sal_Int32 nStartPos );
+
+public:
+ SwASC_AttrIter( SwASCWriter& rWrt, const SwTextNode& rNd, sal_Int32 nStt );
+
+ void NextPos() { m_nCurrentSwPos = SearchNext(m_nCurrentSwPos + 1); }
+
+ sal_Int32 WhereNext() const { return m_nCurrentSwPos; }
+
+ bool OutAttr( sal_Int32 nSwPos );
+};
+
+}
+
+SwASC_AttrIter::SwASC_AttrIter(SwASCWriter& rWr, const SwTextNode& rTextNd, sal_Int32 nStt)
+ : m_rWrt(rWr)
+ , m_rNd(rTextNd)
+ , m_nCurrentSwPos(0)
+{
+ m_nCurrentSwPos = SearchNext(nStt + 1);
+}
+
+sal_Int32 SwASC_AttrIter::SearchNext( sal_Int32 nStartPos )
+{
+ sal_Int32 nMinPos = SAL_MAX_INT32;
+ const SwpHints* pTextAttrs = m_rNd.GetpSwpHints();
+ if( pTextAttrs )
+ {
+ // TODO: This can be optimized, if we make use of the fact that the TextAttrs
+ // are sorted by starting position. We would need to remember two indices, however.
+ for ( size_t i = 0; i < pTextAttrs->Count(); ++i )
+ {
+ const SwTextAttr* pHt = pTextAttrs->Get(i);
+ if ( pHt->HasDummyChar() )
+ {
+ sal_Int32 nPos = pHt->GetStart();
+
+ if( nPos >= nStartPos && nPos <= nMinPos )
+ nMinPos = nPos;
+
+ if( ( ++nPos ) >= nStartPos && nPos < nMinPos )
+ nMinPos = nPos;
+ }
+ else if ( pHt->HasContent() )
+ {
+ const sal_Int32 nHintStart = pHt->GetStart();
+ if ( nHintStart >= nStartPos && nHintStart <= nMinPos )
+ {
+ nMinPos = nHintStart;
+ }
+
+ const sal_Int32 nHintEnd = pHt->End() ? *pHt->End() : COMPLETE_STRING;
+ if ( nHintEnd >= nStartPos && nHintEnd < nMinPos )
+ {
+ nMinPos = nHintEnd;
+ }
+ }
+ }
+ }
+ return nMinPos;
+}
+
+bool SwASC_AttrIter::OutAttr( sal_Int32 nSwPos )
+{
+ bool bRet = false;
+ const SwpHints* pTextAttrs = m_rNd.GetpSwpHints();
+ if( pTextAttrs )
+ {
+ for( size_t i = 0; i < pTextAttrs->Count(); ++i )
+ {
+ const SwTextAttr* pHt = pTextAttrs->Get(i);
+ if ( ( pHt->HasDummyChar()
+ || pHt->HasContent() )
+ && nSwPos == pHt->GetStart() )
+ {
+ bRet = true;
+ OUString sOut;
+ switch( pHt->Which() )
+ {
+ case RES_TXTATR_FIELD:
+ case RES_TXTATR_ANNOTATION:
+ case RES_TXTATR_INPUTFIELD:
+ sOut = static_txtattr_cast<SwTextField const*>(pHt)
+ ->GetFormatField().GetField()->ExpandField(true, nullptr);
+ break;
+
+ case RES_TXTATR_FTN:
+ {
+ const SwFormatFootnote& rFootnote = pHt->GetFootnote();
+ if( !rFootnote.GetNumStr().isEmpty() )
+ sOut = rFootnote.GetNumStr();
+ else if( rFootnote.IsEndNote() )
+ sOut = m_rWrt.m_pDoc->GetEndNoteInfo().m_aFormat.GetNumStr(
+ rFootnote.GetNumber());
+ else
+ sOut = m_rWrt.m_pDoc->GetFootnoteInfo().m_aFormat.GetNumStr(
+ rFootnote.GetNumber());
+ }
+ break;
+ case RES_TXTATR_LINEBREAK:
+ {
+ // Downgrade the clearing break to a simple linebreak.
+ sOut = OUStringChar(GetCharOfTextAttr(*pHt));
+ break;
+ }
+ }
+ if( !sOut.isEmpty() )
+ m_rWrt.Strm().WriteUnicodeOrByteText(sOut);
+ }
+ else if( nSwPos < pHt->GetStart() )
+ break;
+ }
+ }
+ return bRet;
+}
+
+namespace {
+
+class SwASC_RedlineIter
+{
+private:
+ SwTextNode const& m_rNode;
+ IDocumentRedlineAccess const& m_rIDRA;
+ SwRedlineTable::size_type m_nextRedline;
+
+public:
+ SwASC_RedlineIter(SwASCWriter const& rWriter, SwTextNode const& rNode)
+ : m_rNode(rNode)
+ , m_rIDRA(rNode.GetDoc().getIDocumentRedlineAccess())
+ , m_nextRedline(rWriter.m_bHideDeleteRedlines
+ ? m_rIDRA.GetRedlinePos(m_rNode, RedlineType::Delete)
+ : SwRedlineTable::npos)
+ {
+ }
+
+ bool CheckNodeDeleted()
+ {
+ if (m_nextRedline == SwRedlineTable::npos)
+ {
+ return false;
+ }
+ SwRangeRedline const*const pRedline(m_rIDRA.GetRedlineTable()[m_nextRedline]);
+ return pRedline->Start()->GetNodeIndex() < m_rNode.GetIndex()
+ && m_rNode.GetIndex() < pRedline->End()->GetNodeIndex();
+ }
+
+ std::pair<sal_Int32, sal_Int32> GetNextRedlineSkip()
+ {
+ sal_Int32 nRedlineStart(COMPLETE_STRING);
+ sal_Int32 nRedlineEnd(COMPLETE_STRING);
+ for ( ; m_nextRedline < m_rIDRA.GetRedlineTable().size(); ++m_nextRedline)
+ {
+ SwRangeRedline const*const pRedline(m_rIDRA.GetRedlineTable()[m_nextRedline]);
+ if (pRedline->GetType() != RedlineType::Delete)
+ {
+ continue;
+ }
+ auto [pStart, pEnd] = pRedline->StartEnd(); // SwPosition*
+ if (m_rNode.GetIndex() < pStart->GetNodeIndex())
+ {
+ m_nextRedline = SwRedlineTable::npos;
+ break; // done
+ }
+ if (nRedlineStart == COMPLETE_STRING)
+ {
+ nRedlineStart = pStart->GetNodeIndex() == m_rNode.GetIndex()
+ ? pStart->GetContentIndex()
+ : 0;
+ }
+ else
+ {
+ if (pStart->GetContentIndex() != nRedlineEnd)
+ {
+ assert(nRedlineEnd < pStart->GetContentIndex());
+ break; // no increment, revisit it next call
+ }
+ }
+ nRedlineEnd = pEnd->GetNodeIndex() == m_rNode.GetIndex()
+ ? pEnd->GetContentIndex()
+ : COMPLETE_STRING;
+ }
+ return std::make_pair(nRedlineStart, nRedlineEnd);
+ }
+};
+
+}
+
+// Output of the node
+
+static Writer& OutASC_SwTextNode( Writer& rWrt, SwContentNode& rNode )
+{
+ const SwTextNode& rNd = static_cast<SwTextNode&>(rNode);
+
+ sal_Int32 nStrPos = rWrt.m_pCurrentPam->GetPoint()->GetContentIndex();
+ const sal_Int32 nNodeEnd = rNd.Len();
+ sal_Int32 nEnd = nNodeEnd;
+ bool bLastNd = rWrt.m_pCurrentPam->GetPoint()->GetNode() == rWrt.m_pCurrentPam->GetMark()->GetNode();
+ if( bLastNd )
+ nEnd = rWrt.m_pCurrentPam->GetMark()->GetContentIndex();
+
+ bool bIsOneParagraph = rWrt.m_pOrigPam->Start()->GetNode() == rWrt.m_pOrigPam->End()->GetNode() && !getenv("SW_ASCII_COPY_NUMBERING");
+
+ SwASC_AttrIter aAttrIter( static_cast<SwASCWriter&>(rWrt), rNd, nStrPos );
+ SwASC_RedlineIter redlineIter(static_cast<SwASCWriter&>(rWrt), rNd);
+
+ if (redlineIter.CheckNodeDeleted())
+ {
+ return rWrt;
+ }
+
+ const SwNumRule* pNumRule = rNd.GetNumRule();
+ if (pNumRule && !nStrPos && rWrt.m_bExportParagraphNumbering && !bIsOneParagraph)
+ {
+ bool bIsOutlineNumRule = pNumRule == rNd.GetDoc().GetOutlineNumRule();
+
+ // indent each numbering level by 4 spaces
+ OUString level;
+ if (!bIsOutlineNumRule)
+ {
+ for (int i = 0; i <= rNd.GetActualListLevel(); ++i)
+ level += " ";
+ }
+
+ // set up bullets or numbering
+ OUString numString(rNd.GetNumString());
+ if (numString.isEmpty() && !bIsOutlineNumRule)
+ {
+ if (rNd.HasBullet() && !rNd.HasVisibleNumberingOrBullet())
+ numString = " ";
+ else if (rNd.HasBullet())
+ numString = OUString(numfunc::GetBulletChar(rNd.GetActualListLevel()));
+ else if (!rNd.HasBullet() && !rNd.HasVisibleNumberingOrBullet())
+ numString = " ";
+ }
+
+ if (!level.isEmpty() || !numString.isEmpty())
+ rWrt.Strm().WriteUnicodeOrByteText(Concat2View(level + numString + " "));
+ }
+
+ OUString aStr( rNd.GetText() );
+ if( rWrt.m_bASCII_ParaAsBlank )
+ aStr = aStr.replace(0x0A, ' ');
+
+ const bool bExportSoftHyphens = RTL_TEXTENCODING_UCS2 == rWrt.GetAsciiOptions().GetCharSet() ||
+ RTL_TEXTENCODING_UTF8 == rWrt.GetAsciiOptions().GetCharSet();
+
+ std::pair<sal_Int32, sal_Int32> curRedline(redlineIter.GetNextRedlineSkip());
+ for (;;) {
+ const sal_Int32 nNextAttr = std::min(aAttrIter.WhereNext(), nEnd);
+
+ bool isOutAttr(false);
+ if (nStrPos < curRedline.first || curRedline.second <= nStrPos)
+ {
+ isOutAttr = aAttrIter.OutAttr(nStrPos);
+ }
+
+ if (!isOutAttr)
+ {
+ OUStringBuffer buf;
+ while (true)
+ {
+ if (nNextAttr <= curRedline.first)
+ {
+ buf.append(aStr.subView(nStrPos, nNextAttr - nStrPos));
+ break;
+ }
+ else if (nStrPos < curRedline.second)
+ {
+ if (nStrPos < curRedline.first)
+ {
+ buf.append(aStr.subView(nStrPos, curRedline.first - nStrPos));
+ }
+ if (curRedline.second <= nNextAttr)
+ {
+ nStrPos = curRedline.second;
+ curRedline = redlineIter.GetNextRedlineSkip();
+ }
+ else
+ {
+ nStrPos = nNextAttr;
+ break;
+ }
+ }
+ else
+ {
+ curRedline = redlineIter.GetNextRedlineSkip();
+ }
+ }
+ OUString aOutStr(buf.makeStringAndClear());
+ if ( !bExportSoftHyphens )
+ aOutStr = aOutStr.replaceAll(OUStringChar(CHAR_SOFTHYPHEN), "");
+
+ // all INWORD should be already removed by OutAttr
+ // but the field-marks are not attributes so filter those
+ static sal_Unicode const forbidden [] = {
+ CH_TXT_ATR_INPUTFIELDSTART,
+ CH_TXT_ATR_INPUTFIELDEND,
+ CH_TXT_ATR_FORMELEMENT,
+ CH_TXT_ATR_FIELDSTART,
+ CH_TXT_ATR_FIELDSEP,
+ CH_TXT_ATR_FIELDEND,
+ CH_TXTATR_BREAKWORD,
+ 0
+ };
+ aOutStr = comphelper::string::removeAny(aOutStr, forbidden);
+
+ rWrt.Strm().WriteUnicodeOrByteText( aOutStr );
+ }
+ nStrPos = nNextAttr;
+ if (nStrPos >= nEnd)
+ {
+ break;
+ }
+ aAttrIter.NextPos();
+ }
+
+ if( !bLastNd ||
+ ( ( !rWrt.m_bWriteClipboardDoc && !rWrt.m_bASCII_NoLastLineEnd )
+ && !nStrPos && nEnd == nNodeEnd ) )
+ rWrt.Strm().WriteUnicodeOrByteText( static_cast<SwASCWriter&>(rWrt).GetLineEnd());
+
+ return rWrt;
+}
+
+/*
+ * Create the table for the ASCII function pointers to the output
+ * function.
+ * There are local structures that only need to be known to the ASCII DLL.
+ */
+
+SwNodeFnTab aASCNodeFnTab = {
+/* RES_TXTNODE */ OutASC_SwTextNode,
+/* RES_GRFNODE */ nullptr,
+/* RES_OLENODE */ nullptr
+};
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/filter/ascii/parasc.cxx b/sw/source/filter/ascii/parasc.cxx
new file mode 100644
index 0000000000..b4e191df62
--- /dev/null
+++ b/sw/source/filter/ascii/parasc.cxx
@@ -0,0 +1,521 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright
+ * ownership. The ASF licenses this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#include <memory>
+
+#include <tools/stream.hxx>
+#include <hintids.hxx>
+#include <sfx2/docfile.hxx>
+#include <sfx2/printer.hxx>
+#include <sfx2/sfxsids.hrc>
+#include <editeng/fontitem.hxx>
+#include <editeng/langitem.hxx>
+#include <editeng/formatbreakitem.hxx>
+#include <svl/languageoptions.hxx>
+#include <shellio.hxx>
+#include <doc.hxx>
+#include <IDocumentContentOperations.hxx>
+#include <IDocumentDeviceAccess.hxx>
+#include <IDocumentStylePoolAccess.hxx>
+#include <pam.hxx>
+#include <breakit.hxx>
+#include <swerror.h>
+#include <strings.hrc>
+#include <mdiexp.hxx>
+#include <poolfmt.hxx>
+#include <iodetect.hxx>
+
+#include <vcl/metric.hxx>
+#include <osl/diagnose.h>
+
+#define ASC_BUFFLEN 4096
+
+namespace {
+
+class SwASCIIParser
+{
+ SwDoc& m_rDoc;
+ std::optional<SwPaM> m_oPam;
+ SvStream& m_rInput;
+ std::unique_ptr<char[]> m_pArr;
+ const SwAsciiOptions& m_rOpt;
+ SwAsciiOptions m_usedAsciiOptions;
+ std::optional<SfxItemSet> m_oItemSet;
+ tools::Long m_nFileSize;
+ SvtScriptType m_nScript;
+ bool m_bNewDoc;
+
+ ErrCode ReadChars();
+ void InsertText( const OUString& rStr );
+
+ SwASCIIParser(const SwASCIIParser&) = delete;
+ SwASCIIParser& operator=(const SwASCIIParser&) = delete;
+
+public:
+ SwASCIIParser( SwDoc& rD, const SwPaM& rCursor, SvStream& rIn,
+ bool bReadNewDoc, const SwAsciiOptions& rOpts );
+
+ ErrCode CallParser();
+ const SwAsciiOptions& GetUsedAsciiOptions() const { return m_usedAsciiOptions; }
+};
+
+}
+
+// Call for the general reader interface
+ErrCodeMsg AsciiReader::Read( SwDoc& rDoc, const OUString&, SwPaM &rPam, const OUString & )
+{
+ if( !m_pStream )
+ {
+ OSL_ENSURE( false, "ASCII read without a stream" );
+ return ERR_SWG_READ_ERROR;
+ }
+
+ ErrCode nRet;
+ {
+ SwASCIIParser aParser( rDoc, rPam, *m_pStream,
+ !m_bInsertMode, m_aOption.GetASCIIOpts() );
+ nRet = aParser.CallParser();
+
+ OUString optionsString;
+ aParser.GetUsedAsciiOptions().WriteUserData(optionsString);
+
+ if(m_pMedium != nullptr)
+ m_pMedium->GetItemSet().Put(SfxStringItem(SID_FILE_FILTEROPTIONS, optionsString));
+ }
+ // after Read reset the options
+ m_aOption.ResetASCIIOpts();
+ return nRet;
+}
+
+SwASCIIParser::SwASCIIParser(SwDoc& rD, const SwPaM& rCursor, SvStream& rIn, bool bReadNewDoc,
+ const SwAsciiOptions& rOpts)
+ : m_rDoc(rD)
+ , m_rInput(rIn)
+ , m_rOpt(rOpts)
+ , m_usedAsciiOptions(rOpts)
+ , m_nFileSize(0)
+ , m_nScript(SvtScriptType::NONE)
+ , m_bNewDoc(bReadNewDoc)
+{
+ m_oPam.emplace(*rCursor.GetPoint());
+ m_pArr.reset(new char[ASC_BUFFLEN + 2]);
+
+ m_oItemSet.emplace(
+ m_rDoc.GetAttrPool(),
+ svl::Items<RES_CHRATR_FONT, RES_CHRATR_LANGUAGE, RES_CHRATR_CJK_FONT,
+ RES_CHRATR_CJK_LANGUAGE, RES_CHRATR_CTL_FONT, RES_CHRATR_CTL_LANGUAGE>);
+
+ // set defaults from the options
+ if (m_rOpt.GetLanguage())
+ {
+ SvxLanguageItem aLang(m_rOpt.GetLanguage(), RES_CHRATR_LANGUAGE);
+ m_oItemSet->Put(aLang);
+ aLang.SetWhich(RES_CHRATR_CJK_LANGUAGE);
+ m_oItemSet->Put(aLang);
+ aLang.SetWhich(RES_CHRATR_CTL_LANGUAGE);
+ m_oItemSet->Put(aLang);
+ }
+ if (m_rOpt.GetFontName().isEmpty())
+ return;
+
+ vcl::Font aTextFont(m_rOpt.GetFontName(), Size(0, 10));
+ if (m_rDoc.getIDocumentDeviceAccess().getPrinter(false))
+ aTextFont = m_rDoc.getIDocumentDeviceAccess().getPrinter(false)->GetFontMetric(aTextFont);
+ SvxFontItem aFont( aTextFont.GetFamilyType(), aTextFont.GetFamilyName(),
+ OUString(), aTextFont.GetPitch(), aTextFont.GetCharSet(), RES_CHRATR_FONT );
+ m_oItemSet->Put(aFont);
+ aFont.SetWhich(RES_CHRATR_CJK_FONT);
+ m_oItemSet->Put(aFont);
+ aFont.SetWhich(RES_CHRATR_CTL_FONT);
+ m_oItemSet->Put(aFont);
+}
+
+// Calling the parser
+ErrCode SwASCIIParser::CallParser()
+{
+ m_rInput.ResetError();
+ m_nFileSize = m_rInput.TellEnd();
+ m_rInput.Seek(STREAM_SEEK_TO_BEGIN);
+ m_rInput.ResetError();
+
+ ::StartProgress(STR_STATSTR_W4WREAD, 0, m_nFileSize, m_rDoc.GetDocShell());
+
+ std::optional<SwPaM> pInsPam;
+ sal_Int32 nSttContent = 0;
+ if (!m_bNewDoc)
+ {
+ const SwNode& rTmp = m_oPam->GetPoint()->GetNode();
+ pInsPam.emplace( rTmp, rTmp, SwNodeOffset(0), SwNodeOffset(-1) );
+ nSttContent = m_oPam->GetPoint()->GetContentIndex();
+ }
+
+ SwTextFormatColl *pColl = nullptr;
+
+ if (m_bNewDoc)
+ {
+ pColl = m_rDoc.getIDocumentStylePoolAccess().GetTextCollFromPool(RES_POOLCOLL_HTML_PRE,
+ false);
+ if (!pColl)
+ pColl = m_rDoc.getIDocumentStylePoolAccess().GetTextCollFromPool(RES_POOLCOLL_STANDARD,
+ false);
+ if (pColl)
+ m_rDoc.SetTextFormatColl(*m_oPam, pColl);
+ }
+
+ ErrCode nError = ReadChars();
+
+ if (m_oItemSet)
+ {
+ // set only the attribute, for scanned scripts.
+ if (!(SvtScriptType::LATIN & m_nScript))
+ {
+ m_oItemSet->ClearItem(RES_CHRATR_FONT);
+ m_oItemSet->ClearItem(RES_CHRATR_LANGUAGE);
+ }
+ if (!(SvtScriptType::ASIAN & m_nScript))
+ {
+ m_oItemSet->ClearItem(RES_CHRATR_CJK_FONT);
+ m_oItemSet->ClearItem(RES_CHRATR_CJK_LANGUAGE);
+ }
+ if (!(SvtScriptType::COMPLEX & m_nScript))
+ {
+ m_oItemSet->ClearItem(RES_CHRATR_CTL_FONT);
+ m_oItemSet->ClearItem(RES_CHRATR_CTL_LANGUAGE);
+ }
+ if (m_oItemSet->Count())
+ {
+ if (m_bNewDoc)
+ {
+ if (pColl)
+ {
+ // Using the pool defaults for the font causes significant
+ // trouble for the HTML filter, because it is not able
+ // to export the pool defaults (or to be more precise:
+ // the HTML filter is not able to detect whether a pool
+ // default has changed or not. Even a comparison with the
+ // HTML template does not work, because the defaults are
+ // not copied when a new doc is created. The result of
+ // comparing pool defaults therefore would be that the
+ // defaults are exported always if the have changed for
+ // text documents in general. That's not sensible, as well
+ // as it is not sensible to export them always.
+ sal_uInt16 aWhichIds[4] =
+ {
+ RES_CHRATR_FONT, RES_CHRATR_CJK_FONT,
+ RES_CHRATR_CTL_FONT, 0
+ };
+ sal_uInt16 *pWhichIds = aWhichIds;
+ while (*pWhichIds)
+ {
+ const SfxPoolItem *pItem;
+ if (SfxItemState::SET
+ == m_oItemSet->GetItemState(*pWhichIds, false, &pItem))
+ {
+ pColl->SetFormatAttr( *pItem );
+ m_oItemSet->ClearItem(*pWhichIds);
+ }
+ ++pWhichIds;
+ }
+ }
+ if (m_oItemSet->Count())
+ m_rDoc.SetDefault(*m_oItemSet);
+ }
+ else if( pInsPam )
+ {
+ // then set over the insert range the defined attributes
+ *pInsPam->GetMark() = *m_oPam->GetPoint();
+ pInsPam->GetPoint()->Assign(pInsPam->GetPoint()->GetNode(), SwNodeOffset(1),
+ nSttContent );
+
+ // !!!!!
+ OSL_ENSURE( false, "Have to change - hard attr. to para. style" );
+ m_rDoc.getIDocumentContentOperations().InsertItemSet(*pInsPam, *m_oItemSet);
+ }
+ }
+ m_oItemSet.reset();
+ }
+
+ pInsPam.reset();
+
+ ::EndProgress(m_rDoc.GetDocShell());
+ return nError;
+}
+
+ErrCode SwASCIIParser::ReadChars()
+{
+ sal_Unicode *pStt = nullptr, *pEnd = nullptr, *pLastStt = nullptr;
+ tools::Long nReadCnt = 0, nLineLen = 0;
+ sal_Unicode cLastCR = 0;
+ bool bSwapUnicode = false;
+
+ const SwAsciiOptions* pUseMe = &m_rOpt;
+ SwAsciiOptions aEmpty;
+ if (m_nFileSize >= 2 && aEmpty.GetFontName() == m_rOpt.GetFontName()
+ && aEmpty.GetCharSet() == m_rOpt.GetCharSet()
+ && aEmpty.GetLanguage() == m_rOpt.GetLanguage()
+ && aEmpty.GetParaFlags() == m_rOpt.GetParaFlags())
+ {
+ sal_Size nLen, nOrig;
+ nOrig = nLen = m_rInput.ReadBytes(m_pArr.get(), ASC_BUFFLEN);
+ rtl_TextEncoding eCharSet;
+ LineEnd eLineEnd;
+ bool bHasBom;
+ const bool bRet
+ = SwIoSystem::IsDetectableText(m_pArr.get(), nLen, &eCharSet,
+ &bSwapUnicode, &eLineEnd, &bHasBom);
+ if (!bRet)
+ return ERRCODE_IO_BROKENPACKAGE;
+
+ OSL_ENSURE(bRet, "Autodetect of text import without nag dialog must have failed");
+ if (bRet && eCharSet != RTL_TEXTENCODING_DONTKNOW)
+ {
+ aEmpty.SetCharSet(eCharSet);
+ aEmpty.SetParaFlags(eLineEnd);
+ aEmpty.SetIncludeBOM(bHasBom);
+ m_rInput.SeekRel(-(tools::Long(nLen)));
+ }
+ else
+ m_rInput.SeekRel(-(tools::Long(nOrig)));
+ pUseMe=&aEmpty;
+ }
+ m_usedAsciiOptions = *pUseMe;
+
+ rtl_TextToUnicodeConverter hConverter=nullptr;
+ rtl_TextToUnicodeContext hContext=nullptr;
+ rtl_TextEncoding currentCharSet = pUseMe->GetCharSet();
+ if (RTL_TEXTENCODING_UCS2 != currentCharSet)
+ {
+ if( currentCharSet == RTL_TEXTENCODING_DONTKNOW )
+ currentCharSet = RTL_TEXTENCODING_ASCII_US;
+ hConverter = rtl_createTextToUnicodeConverter( currentCharSet );
+ OSL_ENSURE( hConverter, "no string convert available" );
+ if (!hConverter)
+ return ErrCode(ErrCodeArea::Sw, ErrCodeClass::Read, 0);
+ bSwapUnicode = false;
+ hContext = rtl_createTextToUnicodeContext( hConverter );
+ }
+ else if (pUseMe != &aEmpty) //Already successfully figured out type
+ {
+ m_rInput.StartReadingUnicodeText(currentCharSet);
+ bSwapUnicode = m_rInput.IsEndianSwap();
+ }
+
+ std::unique_ptr<sal_Unicode[]> aWork;
+ sal_Size nArrOffset = 0;
+
+ do {
+ if( pStt >= pEnd )
+ {
+ if( pLastStt != pStt )
+ InsertText( OUString( pLastStt ));
+
+ // Read a new block
+ sal_Size lGCount;
+ if (ERRCODE_NONE != m_rInput.GetError()
+ || 0
+ == (lGCount = m_rInput.ReadBytes(m_pArr.get() + nArrOffset,
+ ASC_BUFFLEN - nArrOffset)))
+ break; // break from the while loop
+
+ /*
+ If there was some unconverted bytes on the last cycle then they
+ were put at the beginning of the array, so total bytes available
+ to convert this cycle includes them. If we found 0 following bytes
+ then we ignore the previous partial character.
+ */
+ lGCount += nArrOffset;
+
+ if( hConverter )
+ {
+ sal_uInt32 nInfo;
+ sal_Size nNewLen = lGCount, nCntBytes;
+ aWork.reset(new sal_Unicode[nNewLen + 1]); // add 1 for '\0'
+ sal_Unicode* pBuf = aWork.get();
+ pBuf[nNewLen] = 0; // ensure '\0'
+
+ nNewLen = rtl_convertTextToUnicode(hConverter, hContext, m_pArr.get(), lGCount,
+ pBuf, nNewLen,
+ (RTL_TEXTTOUNICODE_FLAGS_UNDEFINED_DEFAULT
+ | RTL_TEXTTOUNICODE_FLAGS_MBUNDEFINED_DEFAULT
+ | RTL_TEXTTOUNICODE_FLAGS_INVALID_DEFAULT
+ | RTL_TEXTTOUNICODE_FLAGS_GLOBAL_SIGNATURE),
+ &nInfo, &nCntBytes);
+ nArrOffset = lGCount - nCntBytes;
+ if( 0 != nArrOffset )
+ memmove(m_pArr.get(), m_pArr.get() + nCntBytes, nArrOffset);
+
+ pStt = pLastStt = aWork.get();
+ pEnd = pStt + nNewLen;
+ }
+ else
+ {
+ pStt = pLastStt = reinterpret_cast<sal_Unicode*>(m_pArr.get());
+ auto nChars = lGCount / 2;
+ pEnd = pStt + nChars;
+
+ if( bSwapUnicode )
+ {
+ char *pF = m_pArr.get(), *pN = m_pArr.get() + 1;
+ for (sal_Size n = 0; n < nChars; ++n, pF += 2, pN += 2)
+ {
+ char c = *pF;
+ *pF = *pN;
+ *pN = c;
+ }
+ }
+ }
+
+ *pEnd = 0;
+ nReadCnt += lGCount;
+
+ ::SetProgressState(nReadCnt, m_rDoc.GetDocShell());
+
+ if( cLastCR )
+ {
+ if( 0x0a == *pStt && 0x0d == cLastCR )
+ pLastStt = ++pStt;
+ cLastCR = 0;
+ nLineLen = 0;
+ // We skip the last one at the end
+ if (!m_rInput.eof() || !(pEnd == pStt || (!*pEnd && pEnd == pStt + 1)))
+ m_rDoc.getIDocumentContentOperations().SplitNode(*m_oPam->GetPoint(), false);
+ }
+ }
+
+ bool bIns = true, bSplitNode = false;
+ switch( *pStt )
+ {
+
+ case 0x0a: if( LINEEND_LF == pUseMe->GetParaFlags() )
+ {
+ bIns = false;
+ *pStt = 0;
+ ++pStt;
+
+ // We skip the last one at the end
+ if (!m_rInput.eof() || pEnd != pStt)
+ bSplitNode = true;
+ }
+ break;
+
+ case 0x0d: if( LINEEND_LF != pUseMe->GetParaFlags() )
+ {
+ bIns = false;
+ *pStt = 0;
+ ++pStt;
+
+ bool bChkSplit = true;
+ if( LINEEND_CRLF == pUseMe->GetParaFlags() )
+ {
+ if( pStt == pEnd )
+ {
+ cLastCR = 0x0d;
+ bChkSplit = false;
+ }
+ else if( 0x0a == *pStt )
+ ++pStt;
+ }
+
+ // We skip the last one at the end
+ if (bChkSplit && (!m_rInput.eof() || pEnd != pStt))
+ bSplitNode = true;
+ }
+ break;
+
+ case 0x0c:
+ {
+ // Insert a hard page break
+ *pStt++ = 0;
+ if( nLineLen )
+ {
+ InsertText( OUString( pLastStt ));
+ }
+ m_rDoc.getIDocumentContentOperations().SplitNode(*m_oPam->GetPoint(),
+ false);
+ m_rDoc.getIDocumentContentOperations().InsertPoolItem(
+ *m_oPam, SvxFormatBreakItem(SvxBreak::PageBefore, RES_BREAK));
+ pLastStt = pStt;
+ nLineLen = 0;
+ bIns = false;
+ }
+ break;
+
+ case 0x1a:
+ if (nReadCnt == m_nFileSize && pStt + 1 == pEnd)
+ *pStt = 0;
+ else
+ *pStt = '#'; // Replacement visualisation
+ break;
+
+ case '\t': break;
+
+ default:
+ if( ' ' > *pStt )
+ // Found control char, replace with '#'
+ *pStt = '#';
+ break;
+ }
+
+ if( bIns )
+ {
+ if( ( nLineLen >= MAX_ASCII_PARA - 100 ) &&
+ ( ( *pStt == ' ' ) || ( nLineLen >= MAX_ASCII_PARA - 1 ) ) )
+ {
+ sal_Unicode c = *pStt;
+ *pStt = 0;
+ InsertText( OUString( pLastStt ));
+ m_rDoc.getIDocumentContentOperations().SplitNode(*m_oPam->GetPoint(), false);
+ pLastStt = pStt;
+ nLineLen = 0;
+ *pStt = c;
+ }
+ ++pStt;
+ ++nLineLen;
+ }
+ else if( bSplitNode )
+ {
+ // We found a CR/LF, thus save the text
+ InsertText( OUString( pLastStt ));
+ if (m_bNewDoc)
+ m_rDoc.getIDocumentContentOperations().AppendTextNode(*m_oPam->GetPoint());
+ else
+ m_rDoc.getIDocumentContentOperations().SplitNode(*m_oPam->GetPoint(), false);
+ pLastStt = pStt;
+ nLineLen = 0;
+ }
+ } while(true);
+
+ if( hConverter )
+ {
+ rtl_destroyTextToUnicodeContext( hConverter, hContext );
+ rtl_destroyTextToUnicodeConverter( hConverter );
+ }
+ return ERRCODE_NONE;
+}
+
+void SwASCIIParser::InsertText( const OUString& rStr )
+{
+ m_rDoc.getIDocumentContentOperations().InsertString(*m_oPam, rStr);
+
+ if (m_oItemSet && g_pBreakIt
+ && m_nScript != (SvtScriptType::LATIN | SvtScriptType::ASIAN | SvtScriptType::COMPLEX))
+ m_nScript |= g_pBreakIt->GetAllScriptsOfText(rStr);
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/filter/ascii/wrtasc.cxx b/sw/source/filter/ascii/wrtasc.cxx
new file mode 100644
index 0000000000..0f1e368b92
--- /dev/null
+++ b/sw/source/filter/ascii/wrtasc.cxx
@@ -0,0 +1,230 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright
+ * ownership. The ASF licenses this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#include <osl/endian.h>
+#include <tools/stream.hxx>
+#include <pam.hxx>
+#include <doc.hxx>
+#include <ndtxt.hxx>
+#include <mdiexp.hxx>
+#include <fmtcntnt.hxx>
+#include <frmfmt.hxx>
+#include "wrtasc.hxx"
+#include <frameformats.hxx>
+#include <sfx2/docfile.hxx>
+#include <sfx2/sfxsids.hrc>
+#include <o3tl/string_view.hxx>
+
+#include <strings.hrc>
+
+SwASCWriter::SwASCWriter( std::u16string_view rFltNm )
+{
+ SwAsciiOptions aNewOpts;
+
+ switch( 5 <= rFltNm.size() ? rFltNm[4] : 0 )
+ {
+ case 'D':
+ aNewOpts.SetCharSet( RTL_TEXTENCODING_IBM_850 );
+ aNewOpts.SetParaFlags( LINEEND_CRLF );
+ if( 5 < rFltNm.size() )
+ {
+ std::u16string_view aFilterNum = rFltNm.substr( 5 );
+ switch( o3tl::toInt32(aFilterNum) )
+ {
+ case 437: aNewOpts.SetCharSet( RTL_TEXTENCODING_IBM_437 ); break;
+ case 850: aNewOpts.SetCharSet( RTL_TEXTENCODING_IBM_850 ); break;
+ case 860: aNewOpts.SetCharSet( RTL_TEXTENCODING_IBM_860 ); break;
+ case 861: aNewOpts.SetCharSet( RTL_TEXTENCODING_IBM_861 ); break;
+ case 863: aNewOpts.SetCharSet( RTL_TEXTENCODING_IBM_863 ); break;
+ case 865: aNewOpts.SetCharSet( RTL_TEXTENCODING_IBM_865 ); break;
+ }
+ }
+ break;
+
+ case 'A':
+#ifndef _WIN32
+ aNewOpts.SetCharSet( RTL_TEXTENCODING_MS_1252 );
+ aNewOpts.SetParaFlags( LINEEND_CRLF );
+#endif
+ break;
+
+ case 'M':
+ aNewOpts.SetCharSet( RTL_TEXTENCODING_APPLE_ROMAN );
+ aNewOpts.SetParaFlags( LINEEND_CR );
+ break;
+
+ case 'X':
+#ifdef _WIN32
+ aNewOpts.SetCharSet( RTL_TEXTENCODING_MS_1252 );
+ aNewOpts.SetParaFlags( LINEEND_LF );
+#endif
+ break;
+
+ default:
+ if( rFltNm.size() >= 4 && rFltNm.substr( 4 )==u"_DLG" )
+ {
+ // use the options
+ aNewOpts = GetAsciiOptions();
+ }
+ }
+ SetAsciiOptions( aNewOpts );
+}
+
+SwASCWriter::~SwASCWriter() {}
+
+ErrCode SwASCWriter::WriteStream()
+{
+ static constexpr OUString STR_CR = u"\015"_ustr;
+ static constexpr OUStringLiteral STR_LF = u"\012";
+ static constexpr OUStringLiteral STR_CRLF = u"\015\012";
+ static constexpr OUStringLiteral STR_BLANK = u" ";
+ bool bIncludeBOM = GetAsciiOptions().GetIncludeBOM();
+ bool bIncludeHidden = GetAsciiOptions().GetIncludeHidden();
+
+ if( m_bASCII_ParaAsCR ) // If predefined
+ m_sLineEnd = STR_CR;
+ else if( m_bASCII_ParaAsBlank )
+ m_sLineEnd = STR_BLANK;
+ else
+ switch( GetAsciiOptions().GetParaFlags() )
+ {
+ case LINEEND_CR: m_sLineEnd = STR_CR; break;
+ case LINEEND_LF: m_sLineEnd = STR_LF; break;
+ case LINEEND_CRLF: m_sLineEnd = STR_CRLF; break;
+ }
+
+ SwNodeOffset nMaxNode = m_pDoc->GetNodes().Count();
+
+ if( m_bShowProgress )
+ ::StartProgress( STR_STATSTR_W4WWRITE, 0, sal_Int32(nMaxNode), m_pDoc->GetDocShell() );
+
+ SwPaM* pPam = m_pOrigPam;
+
+ bool bWriteSttTag = m_bUCS2_WithStartChar &&
+ (RTL_TEXTENCODING_UCS2 == GetAsciiOptions().GetCharSet() ||
+ RTL_TEXTENCODING_UTF8 == GetAsciiOptions().GetCharSet());
+
+ rtl_TextEncoding eOld = Strm().GetStreamCharSet();
+ Strm().SetStreamCharSet( GetAsciiOptions().GetCharSet() );
+
+ // Output all areas of the pam into the ASC file
+ do {
+ bool bTstFly = true;
+ while( m_pCurrentPam->GetPoint()->GetNodeIndex() < m_pCurrentPam->GetMark()->GetNodeIndex() ||
+ (m_pCurrentPam->GetPoint()->GetNodeIndex() == m_pCurrentPam->GetMark()->GetNodeIndex() &&
+ m_pCurrentPam->GetPoint()->GetContentIndex() <= m_pCurrentPam->GetMark()->GetContentIndex()) )
+ {
+ SwTextNode* pNd = m_pCurrentPam->GetPoint()->GetNode().GetTextNode();
+ if( pNd )
+ {
+ // Should we have frames only?
+ // That's possible, if we put a frame selection into the clipboard
+ if( bTstFly && m_bWriteAll &&
+ pNd->GetText().isEmpty() &&
+ // Frame exists
+ !m_pDoc->GetSpzFrameFormats()->empty() &&
+ // Only one node in the array
+ m_pDoc->GetNodes().GetEndOfExtras().GetIndex() + 3 ==
+ m_pDoc->GetNodes().GetEndOfContent().GetIndex() &&
+ // And exactly this one is selected
+ m_pDoc->GetNodes().GetEndOfContent().GetIndex() - 1 ==
+ m_pCurrentPam->GetPoint()->GetNodeIndex() )
+ {
+ // Print the frame's content.
+ // It is always at position 0!
+ const SwFrameFormat* pFormat = (*m_pDoc->GetSpzFrameFormats())[ 0 ];
+ const SwNodeIndex* pIdx = pFormat->GetContent().GetContentIdx();
+ if( pIdx )
+ {
+ m_pCurrentPam = NewUnoCursor(*m_pDoc, pIdx->GetIndex(),
+ pIdx->GetNode().EndOfSectionIndex() );
+ m_pCurrentPam->Exchange();
+ continue; // reset while loop!
+ }
+ }
+ else if (!pNd->IsHidden() || bIncludeHidden)
+ {
+ if (bWriteSttTag)
+ {
+ switch(GetAsciiOptions().GetCharSet())
+ {
+ case RTL_TEXTENCODING_UTF8:
+ if( bIncludeBOM )
+ {
+ Strm().WriteUChar( 0xEF ).WriteUChar( 0xBB ).WriteUChar( 0xBF );
+ }
+
+ break;
+ case RTL_TEXTENCODING_UCS2:
+#ifdef OSL_LITENDIAN
+ Strm().SetEndian(SvStreamEndian::LITTLE);
+#else
+ Strm().SetEndian(SvStreamEndian::BIG);
+#endif
+ if( bIncludeBOM )
+ {
+ Strm().StartWritingUnicodeText();
+ }
+ break;
+
+ }
+ bWriteSttTag = false;
+ }
+ Out( aASCNodeFnTab, *pNd, *this );
+ }
+ bTstFly = false; // Testing once is enough
+ }
+
+ if( !m_pCurrentPam->Move( fnMoveForward, GoInNode ) )
+ break;
+
+ if( m_bShowProgress )
+ ::SetProgressState( sal_Int32(m_pCurrentPam->GetPoint()->GetNodeIndex()),
+ m_pDoc->GetDocShell() ); // How far?
+
+ }
+ } while( CopyNextPam( &pPam ) ); // Until all pams are processed
+
+ Strm().SetStreamCharSet( eOld );
+
+ if( m_bShowProgress )
+ ::EndProgress( m_pDoc->GetDocShell() );
+
+ return ERRCODE_NONE;
+}
+
+void SwASCWriter::SetupFilterOptions(SfxMedium& rMedium)
+{
+ if( const SfxStringItem* pItem = rMedium.GetItemSet().GetItemIfSet( SID_FILE_FILTEROPTIONS ) )
+ {
+ SwAsciiOptions aOpt;
+ OUString sItemOpt;
+ sItemOpt = pItem->GetValue();
+ aOpt.ReadUserData(sItemOpt);
+ SetAsciiOptions(aOpt);
+ }
+}
+
+void GetASCWriter(
+ std::u16string_view rFltNm, [[maybe_unused]] const OUString& /*rBaseURL*/, WriterRef& xRet )
+{
+ xRet = new SwASCWriter( rFltNm );
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/filter/ascii/wrtasc.hxx b/sw/source/filter/ascii/wrtasc.hxx
new file mode 100644
index 0000000000..c2e3dfa9a2
--- /dev/null
+++ b/sw/source/filter/ascii/wrtasc.hxx
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright
+ * ownership. The ASF licenses this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+#ifndef INCLUDED_SW_SOURCE_FILTER_ASCII_WRTASC_HXX
+#define INCLUDED_SW_SOURCE_FILTER_ASCII_WRTASC_HXX
+
+#include <shellio.hxx>
+#include <wrt_fn.hxx>
+
+extern SwNodeFnTab aASCNodeFnTab;
+
+// The ASC writer
+
+class SwASCWriter : public Writer
+{
+ OUString m_sLineEnd;
+
+ virtual ErrCode WriteStream() override;
+
+public:
+ SwASCWriter(std::u16string_view rFilterName);
+ virtual ~SwASCWriter() override;
+
+ void SetupFilterOptions(SfxMedium& rMedium) override;
+ const OUString& GetLineEnd() const { return m_sLineEnd; }
+};
+
+#endif // _ INCLUDED_SW_SOURCE_FILTER_ASCII_WRTASC_HXX
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */