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 <hintids.hxx>
21 #include <vcl/font.hxx>
22 #include <editeng/langitem.hxx>
25 #include <numrule.hxx>
26 #include <charfmt.hxx>
27 #include <com/sun/star/i18n/ScriptType.hpp>
29 #include "sprmids.hxx"
31 #include "ww8attributeoutput.hxx"
32 #include "writerhelper.hxx"
33 #include "writerwordglue.hxx"
37 using namespace ::com::sun::star
;
38 using namespace sw::types
;
39 using namespace sw::util
;
41 SwNumRule
* MSWordExportBase::DuplicateNumRuleImpl(const SwNumRule
*pRule
)
43 const OUString
sPrefix("WW8TempExport" + OUString::number( m_nUniqueList
++ ));
44 SwNumRule
* pMyNumRule
=
45 new SwNumRule( m_rDoc
.GetUniqueNumRuleName( &sPrefix
),
46 SvxNumberFormat::LABEL_WIDTH_AND_POSITION
);
47 m_pUsedNumTable
->push_back( pMyNumRule
);
49 for ( sal_uInt16 i
= 0; i
< MAXLEVEL
; i
++ )
51 const SwNumFormat
& rSubRule
= pRule
->Get(i
);
52 pMyNumRule
->Set( i
, rSubRule
);
57 sal_uInt16
MSWordExportBase::DuplicateNumRule(const SwNumRule
* pRule
, sal_uInt8 nLevel
, sal_uInt16 nVal
)
59 SwNumRule
* const pMyNumRule
= DuplicateNumRuleImpl(pRule
);
61 SwNumFormat
aNumFormat(pMyNumRule
->Get(nLevel
));
62 aNumFormat
.SetStart(nVal
);
63 pMyNumRule
->Set(nLevel
, aNumFormat
);
65 return GetNumberingId(*pMyNumRule
);
68 // multiple SwList can be based on the same SwNumRule; ensure one w:abstractNum
70 sal_uInt16
MSWordExportBase::DuplicateAbsNum(OUString
const& rListId
,
71 SwNumRule
const& rAbstractRule
)
73 auto const it(m_Lists
.find(rListId
));
74 if (it
!= m_Lists
.end())
80 auto const pNewAbstractRule
= DuplicateNumRuleImpl(&rAbstractRule
);
81 assert(GetNumberingId(*pNewAbstractRule
) == m_pUsedNumTable
->size() - 1);
82 (void) pNewAbstractRule
;
83 m_Lists
.insert(std::make_pair(rListId
, m_pUsedNumTable
->size() - 1));
84 return m_pUsedNumTable
->size() - 1;
88 // Ideally we want to map SwList to w:abstractNum and SwNumRule to w:num
89 // The current approach is to keep exporting every SwNumRule to
90 // 1 w:abstractNum and 1 w:num, and then add extra w:num via this function
91 // that reference an existing w:abstractNum and may override its formatting;
92 // of course this will end up exporting some w:num that aren't actually used.
93 sal_uInt16
MSWordExportBase::OverrideNumRule(
94 SwNumRule
const& rExistingRule
,
95 OUString
const& rListId
,
96 SwNumRule
const& rAbstractRule
)
98 const sal_uInt16 numdef
= GetNumberingId(rExistingRule
);
100 const sal_uInt16 absnumdef
= rListId
== rAbstractRule
.GetDefaultListId()
101 ? GetNumberingId(rAbstractRule
)
102 : DuplicateAbsNum(rListId
, rAbstractRule
);
103 assert(numdef
!= USHRT_MAX
);
104 assert(absnumdef
!= USHRT_MAX
);
105 auto const mapping
= std::make_pair(numdef
, absnumdef
);
107 auto it
= m_OverridingNums
.insert(std::make_pair(m_pUsedNumTable
->size(), mapping
));
109 m_pUsedNumTable
->push_back(nullptr); // dummy, it's unique_ptr...
110 ++m_nUniqueList
; // counter for DuplicateNumRule...
112 return it
.first
->first
;
115 void MSWordExportBase::AddListLevelOverride(sal_uInt16 nListId
,
116 sal_uInt16 nLevelNum
,
119 m_ListLevelOverrides
[nListId
][nLevelNum
] = nStartAt
;
122 sal_uInt16
MSWordExportBase::GetNumberingId( const SwNumRule
& rNumRule
)
124 if ( !m_pUsedNumTable
)
126 m_pUsedNumTable
.reset(new SwNumRuleTable
);
127 m_pUsedNumTable
->insert( m_pUsedNumTable
->begin(), m_rDoc
.GetNumRuleTable().begin(), m_rDoc
.GetNumRuleTable().end() );
128 // Check, if the outline rule is already inserted into <pUsedNumTable>.
129 // If yes, do not insert it again.
130 bool bOutlineRuleAdded( false );
131 for ( sal_uInt16 n
= m_pUsedNumTable
->size(); n
; )
133 const SwNumRule
& rRule
= *(*m_pUsedNumTable
)[ --n
];
134 if (!m_rDoc
.IsUsed(rRule
))
136 m_pUsedNumTable
->erase( m_pUsedNumTable
->begin() + n
);
138 else if ( &rRule
== m_rDoc
.GetOutlineNumRule() )
140 bOutlineRuleAdded
= true;
144 if ( !bOutlineRuleAdded
)
146 // still need to paste the OutlineRule
147 SwNumRule
* pR
= m_rDoc
.GetOutlineNumRule();
148 m_pUsedNumTable
->push_back( pR
);
151 SwNumRule
* p
= const_cast<SwNumRule
*>(&rNumRule
);
152 sal_uInt16 nRet
= o3tl::narrowing
<sal_uInt16
>(m_pUsedNumTable
->GetPos(p
));
157 // GetFirstLineOffset should problem never appear unadorned apart from
158 // here in the ww export filter
159 sal_Int16
GetWordFirstLineOffset(const SwNumFormat
&rFormat
)
161 OSL_ENSURE( rFormat
.GetPositionAndSpaceMode() == SvxNumberFormat::LABEL_WIDTH_AND_POSITION
,
162 "<GetWordFirstLineOffset> - misusage: position-and-space-mode does not equal LABEL_WIDTH_AND_POSITION" );
164 short nFirstLineOffset
;
165 if (rFormat
.GetNumAdjust() == SvxAdjust::Right
)
166 nFirstLineOffset
= -rFormat
.GetCharTextDistance();
168 nFirstLineOffset
= rFormat
.GetFirstLineOffset(); //TODO: overflow
169 return nFirstLineOffset
;
172 void WW8Export::WriteNumbering()
174 if ( !m_pUsedNumTable
)
175 return; // no numbering is used
177 // list formats - LSTF
178 m_pFib
->m_fcPlcfLst
= m_pTableStrm
->Tell();
179 m_pTableStrm
->WriteUInt16( m_pUsedNumTable
->size() );
180 NumberingDefinitions();
182 m_pFib
->m_lcbPlcfLst
= m_pTableStrm
->Tell() - m_pFib
->m_fcPlcfLst
;
184 // list formats - LVLF
185 AbstractNumberingDefinitions();
187 // list formats - LFO
188 OutOverrideListTab();
190 // list formats - ListNames
194 void WW8AttributeOutput::NumberingDefinition( sal_uInt16 nId
, const SwNumRule
&rRule
)
196 m_rWW8Export
.m_pTableStrm
->WriteUInt32( nId
);
197 m_rWW8Export
.m_pTableStrm
->WriteUInt32( nId
);
199 // not associated with a Style
200 for ( int i
= 0; i
< WW8ListManager::nMaxLevel
; ++i
)
201 m_rWW8Export
.m_pTableStrm
->WriteUInt16( 0xFFF );
203 sal_uInt8 nFlags
= 0;
204 if ( rRule
.IsContinusNum() )
207 m_rWW8Export
.m_pTableStrm
->WriteUChar( nFlags
).WriteUChar( 0/*nDummy*/ );
210 void MSWordExportBase::NumberingDefinitions()
212 if ( !m_pUsedNumTable
)
213 return; // no numbering is used
215 sal_uInt16 nCount
= m_pUsedNumTable
->size();
217 // Write static data of SwNumRule - LSTF
218 for ( sal_uInt16 n
= 0; n
< nCount
; ++n
)
220 const SwNumRule
* pRule
= (*m_pUsedNumTable
)[ n
];
223 AttrOutput().NumberingDefinition(n
+ 1, *pRule
);
227 auto it
= m_OverridingNums
.find(n
);
228 assert(it
!= m_OverridingNums
.end());
229 pRule
= (*m_pUsedNumTable
)[it
->second
.first
];
231 AttrOutput().OverrideNumberingDefinition(*pRule
, n
+ 1, it
->second
.second
+ 1, m_ListLevelOverrides
[n
]);
237 * Converts the SVX numbering type to MSONFC.
239 * This is used for special paragraph numbering considerations.
241 static sal_uInt8
GetLevelNFC(sal_uInt16 eNumType
, const SfxItemSet
* pOutSet
, sal_uInt8 nDefault
)
243 sal_uInt8 nRet
= nDefault
;
246 case SVX_NUM_NUMBER_LOWER_ZH
:
249 const SvxLanguageItem
& rLang
= pOutSet
->Get( RES_CHRATR_CJK_LANGUAGE
);
250 const LanguageType eLang
= rLang
.GetLanguage();
251 if (LANGUAGE_CHINESE_SIMPLIFIED
==eLang
) {
257 // LVLF can't contain 0x08, msonfcHex.
258 case style::NumberingType::SYMBOL_CHICAGO
:
259 // No SVX_NUM_SYMBOL_CHICAGO here: LVLF can't contain 0x09, msonfcChiManSty.
262 // LVLF can't contain 0x0F / 15, msonfcSbChar / decimalHalfWidth.
263 // LVLF can't contain 0x13 / 19, msonfcDArabic / decimalFullWidth2
269 void WW8AttributeOutput::NumberingLevel( sal_uInt8
/*nLevel*/,
271 sal_uInt16 nNumberingType
,
273 const sal_uInt8
*pNumLvlPos
,
276 const SfxItemSet
*pOutSet
,
278 sal_Int16 nFirstLineIndex
,
279 sal_Int16 nListTabPos
,
280 const OUString
&rNumberingString
,
281 const SvxBrushItem
* pBrush
//For i120928,to transfer graphic of bullet
285 m_rWW8Export
.m_pTableStrm
->WriteUInt32( nStart
);
288 sal_uInt8 nNumId
= GetLevelNFC(nNumberingType
, pOutSet
, WW8Export::GetNumId(nNumberingType
));
289 m_rWW8Export
.m_pTableStrm
->WriteUChar(nNumId
);
295 case SvxAdjust::Center
:
298 case SvxAdjust::Right
:
305 m_rWW8Export
.m_pTableStrm
->WriteUChar( nAlign
);
307 // Write the rgbxchNums[9], positions of placeholders for paragraph
308 // numbers in the text
309 m_rWW8Export
.m_pTableStrm
->WriteBytes(pNumLvlPos
, WW8ListManager::nMaxLevel
);
311 // Type of the character between the bullet and the text
312 m_rWW8Export
.m_pTableStrm
->WriteUChar( nFollow
);
314 // dxaSoace/dxaIndent (Word 6 compatibility)
315 m_rWW8Export
.m_pTableStrm
->WriteUInt32( 0 );
316 m_rWW8Export
.m_pTableStrm
->WriteUInt32( 0 );
319 std::unique_ptr
<ww::bytes
> pCharAtrs
;
322 std::unique_ptr
<ww::bytes
> pOldpO
= std::move(m_rWW8Export
.m_pO
);
323 m_rWW8Export
.m_pO
.reset(new ww::bytes
);
326 sal_uInt16 nFontID
= m_rWW8Export
.m_aFontHelper
.GetId( *pFont
);
328 m_rWW8Export
.InsUInt16( NS_sprm::CRgFtc0::val
);
329 m_rWW8Export
.InsUInt16( nFontID
);
330 m_rWW8Export
.InsUInt16( NS_sprm::CRgFtc2::val
);
331 m_rWW8Export
.InsUInt16( nFontID
);
334 m_rWW8Export
.OutputItemSet( *pOutSet
, false, true, i18n::ScriptType::LATIN
, m_rWW8Export
.m_bExportModeRTF
);
335 //For i120928,achieve graphic's index of bullet from the bullet bookmark
336 if (SVX_NUM_BITMAP
== nNumberingType
&& pBrush
)
338 int nIndex
= m_rWW8Export
.GetGrfIndex(*pBrush
);
341 m_rWW8Export
.InsUInt16(NS_sprm::CPbiIBullet::val
);
342 m_rWW8Export
.InsUInt32(nIndex
);
343 m_rWW8Export
.InsUInt16(NS_sprm::CPbiGrf::val
);
344 m_rWW8Export
.InsUInt16(1);
348 pCharAtrs
= std::move(m_rWW8Export
.m_pO
);
349 m_rWW8Export
.m_pO
= std::move(pOldpO
);
351 m_rWW8Export
.m_pTableStrm
->WriteUChar(sal_uInt8(pCharAtrs
? pCharAtrs
->size() : 0));
354 sal_uInt8 aPapSprms
[] = {
355 0x5e, 0x84, 0, 0, // sprmPDxaLeft
356 0x60, 0x84, 0, 0, // sprmPDxaLeft1
357 0x15, 0xc6, 0x05, 0x00, 0x01, 0, 0, 0x06
359 m_rWW8Export
.m_pTableStrm
->WriteUChar( sal_uInt8( sizeof( aPapSprms
) ) );
362 m_rWW8Export
.m_pTableStrm
->WriteUInt16( 0 );
365 sal_uInt8
* pData
= aPapSprms
+ 2;
366 Set_UInt16( pData
, nIndentAt
);
368 Set_UInt16( pData
, nFirstLineIndex
);
370 Set_UInt16( pData
, nListTabPos
);
372 m_rWW8Export
.m_pTableStrm
->WriteBytes(aPapSprms
, sizeof(aPapSprms
));
375 if (pCharAtrs
&& !pCharAtrs
->empty())
376 m_rWW8Export
.m_pTableStrm
->WriteBytes(pCharAtrs
->data(), pCharAtrs
->size());
378 // write the num string
379 m_rWW8Export
.m_pTableStrm
->WriteUInt16( rNumberingString
.getLength() );
380 SwWW8Writer::WriteString16( *m_rWW8Export
.m_pTableStrm
, rNumberingString
, false );
383 void MSWordExportBase::AbstractNumberingDefinitions()
385 sal_uInt16 nCount
= m_pUsedNumTable
->size();
388 for( n
= 0; n
< nCount
; ++n
)
390 if (nullptr == (*m_pUsedNumTable
)[ n
])
395 AttrOutput().StartAbstractNumbering( n
+ 1 );
397 const SwNumRule
& rRule
= *(*m_pUsedNumTable
)[ n
];
399 sal_uInt8 nLevels
= static_cast< sal_uInt8
>(rRule
.IsContinusNum() ?
400 WW8ListManager::nMinLevel
: WW8ListManager::nMaxLevel
);
401 for( nLvl
= 0; nLvl
< nLevels
; ++nLvl
)
403 NumberingLevel(rRule
, nLvl
);
406 AttrOutput().EndAbstractNumbering();
410 void MSWordExportBase::NumberingLevel(
411 SwNumRule
const& rRule
, sal_uInt8
const nLvl
)
413 // write the static data of the SwNumFormat of this level
414 sal_uInt8 aNumLvlPos
[WW8ListManager::nMaxLevel
] = { 0,0,0,0,0,0,0,0,0 };
416 const SwNumFormat
& rFormat
= rRule
.Get( nLvl
);
418 sal_uInt8 nFollow
= 0;
420 if (rFormat
.GetPositionAndSpaceMode() == SvxNumberFormat::LABEL_WIDTH_AND_POSITION
)
422 // <nFollow = 2>, if minimum label width equals 0 and
423 // minimum distance between label and text equals 0
424 nFollow
= (rFormat
.GetFirstLineOffset() == 0 &&
425 rFormat
.GetCharTextDistance() == 0)
426 ? 2 : 0; // ixchFollow: 0 - tab, 1 - blank, 2 - nothing
428 else if (rFormat
.GetPositionAndSpaceMode() == SvxNumberFormat::LABEL_ALIGNMENT
)
430 switch (rFormat
.GetLabelFollowedBy())
432 case SvxNumberFormat::LISTTAB
:
434 // 0 (tab) unless there would be no content before the tab, in which case 2 (nothing)
435 nFollow
= (SVX_NUM_NUMBER_NONE
!= rFormat
.GetNumberingType()) ? 0 : 2;
438 case SvxNumberFormat::SPACE
:
440 // 1 (space) unless there would be no content before the space in which case 2 (nothing)
441 nFollow
= (SVX_NUM_NUMBER_NONE
!= rFormat
.GetNumberingType()) ? 1 : 2;
444 case SvxNumberFormat::NOTHING
:
452 OSL_FAIL( "unknown GetLabelFollowedBy() return value" );
457 // Build the NumString for this Level
460 bool bWriteBullet
= false;
461 std::optional
<vcl::Font
> pBulletFont
;
462 rtl_TextEncoding eChrSet
=0;
463 FontFamily eFamily
=FAMILY_DECORATIVE
;
464 if (SVX_NUM_CHAR_SPECIAL
== rFormat
.GetNumberingType() ||
465 SVX_NUM_BITMAP
== rFormat
.GetNumberingType())
468 sal_UCS4 cBullet
= rFormat
.GetBulletChar();
469 sNumStr
= OUString(&cBullet
, 1);
473 // Create level string
474 if (rFormat
.HasListFormat())
476 sal_uInt8
* pLvlPos
= aNumLvlPos
;
477 sNumStr
= rFormat
.GetListFormat();
479 // now search the nums in the string
480 for (sal_uInt8 i
= 0; i
<= nLvl
; ++i
)
482 OUString
sSrch("%" + OUString::number(i
+1) + "%");
483 sal_Int32 nFnd
= sNumStr
.indexOf(sSrch
);
486 *pLvlPos
= static_cast<sal_uInt8
>(nFnd
+ 1);
488 sNumStr
= sNumStr
.replaceAt(nFnd
, sSrch
.getLength(), rtl::OUStringChar(static_cast<char>(i
)));
492 else if (rFormat
.GetNumberingType() != SVX_NUM_NUMBER_NONE
)
493 assert(false && "deprecated format still exists and is unhandled. Inform Vasily or Justin");
496 if (SVX_NUM_CHAR_SPECIAL
== rFormat
.GetNumberingType() ||
497 SVX_NUM_BITMAP
== rFormat
.GetNumberingType())
501 pBulletFont
= rFormat
.GetBulletFont();
504 pBulletFont
= numfunc::GetDefBulletFont();
507 eChrSet
= pBulletFont
->GetCharSet();
508 sFontName
= pBulletFont
->GetFamilyName();
509 eFamily
= pBulletFont
->GetFamilyType();
511 if (IsOpenSymbol(sFontName
))
512 SubstituteBullet(sNumStr
, eChrSet
, sFontName
);
515 // Attributes of the numbering
516 std::unique_ptr
<wwFont
> pPseudoFont
;
517 const SfxItemSet
* pOutSet
= nullptr;
520 SfxItemSetFixed
<RES_CHRATR_BEGIN
, RES_CHRATR_END
> aSet( m_rDoc
.GetAttrPool() );
521 if (rFormat
.GetCharFormat() || bWriteBullet
)
527 if (rFormat
.GetCharFormat())
528 aSet
.Put( rFormat
.GetCharFormat()->GetAttrSet() );
529 aSet
.ClearItem( RES_CHRATR_CJK_FONT
);
530 aSet
.ClearItem( RES_CHRATR_FONT
);
532 if (sFontName
.isEmpty())
533 sFontName
= pBulletFont
->GetFamilyName();
535 pPseudoFont
.reset(new wwFont( sFontName
, pBulletFont
->GetPitch(),
539 pOutSet
= &rFormat
.GetCharFormat()->GetAttrSet();
542 sal_Int16 nIndentAt
= 0;
543 sal_Int16 nFirstLineIndex
= 0;
544 sal_Int16 nListTabPos
= -1;
547 if (rFormat
.GetPositionAndSpaceMode() == SvxNumberFormat::LABEL_WIDTH_AND_POSITION
)
549 nIndentAt
= nListTabPos
= rFormat
.GetAbsLSpace(); //TODO: overflow
550 nFirstLineIndex
= GetWordFirstLineOffset(rFormat
);
552 else if (rFormat
.GetPositionAndSpaceMode() == SvxNumberFormat::LABEL_ALIGNMENT
)
554 nIndentAt
= static_cast<sal_Int16
>(rFormat
.GetIndentAt());
555 nFirstLineIndex
= static_cast<sal_Int16
>(rFormat
.GetFirstLineIndent());
556 nListTabPos
= rFormat
.GetLabelFollowedBy() == SvxNumberFormat::LISTTAB
?
557 static_cast<sal_Int16
>( rFormat
.GetListtabPos() ) : 0;
560 AttrOutput().NumberingLevel( nLvl
,
562 rFormat
.GetNumberingType(),
563 rFormat
.GetNumAdjust(),
566 pPseudoFont
.get(), pOutSet
,
567 nIndentAt
, nFirstLineIndex
, nListTabPos
,
569 rFormat
.GetNumberingType()==SVX_NUM_BITMAP
? rFormat
.GetBrush() : nullptr);
572 void WW8Export::OutOverrideListTab()
574 if( !m_pUsedNumTable
)
575 return ; // no numbering is used
577 // write the "list format override" - LFO
578 sal_uInt16 nCount
= m_pUsedNumTable
->size();
581 m_pFib
->m_fcPlfLfo
= m_pTableStrm
->Tell();
582 m_pTableStrm
->WriteUInt32( nCount
);
584 // LFO ([MS-DOC] 2.9.131)
585 for( n
= 0; n
< nCount
; ++n
)
587 m_pTableStrm
->WriteUInt32( n
+ 1 );
588 SwWW8Writer::FillCount( *m_pTableStrm
, 12 );
590 // LFOData ([MS-DOC] 2.9.132)
591 for( n
= 0; n
< nCount
; ++n
)
592 m_pTableStrm
->WriteInt32( -1 ); // no overwrite
595 m_pFib
->m_lcbPlfLfo
= m_pTableStrm
->Tell() - m_pFib
->m_fcPlfLfo
;
598 void WW8Export::OutListNamesTab()
600 if( !m_pUsedNumTable
)
601 return ; // no numbering is used
603 // write the "list format override" - LFO
604 sal_uInt16 nNms
= 0, nCount
= m_pUsedNumTable
->size();
606 m_pFib
->m_fcSttbListNames
= m_pTableStrm
->Tell();
607 m_pTableStrm
->WriteInt16( -1 );
608 m_pTableStrm
->WriteUInt32( nCount
);
610 for( ; nNms
< nCount
; ++nNms
)
612 const SwNumRule
& rRule
= *(*m_pUsedNumTable
)[ nNms
];
614 if( !rRule
.IsAutoRule() )
615 sNm
= rRule
.GetName();
617 m_pTableStrm
->WriteUInt16( sNm
.getLength() );
619 SwWW8Writer::WriteString16(*m_pTableStrm
, sNm
, false);
622 SwWW8Writer::WriteLong( *m_pTableStrm
, m_pFib
->m_fcSttbListNames
+ 2, nNms
);
624 m_pFib
->m_lcbSttbListNames
= m_pTableStrm
->Tell() - m_pFib
->m_fcSttbListNames
;
627 void MSWordExportBase::SubstituteBullet( OUString
& rNumStr
,
628 rtl_TextEncoding
& rChrSet
, OUString
& rFontName
) const
630 if (!m_bSubstituteBullets
)
632 OUString sFontName
= rFontName
;
634 // If Bullet char is "", don't change
635 if (rNumStr
[0] != u
'\0')
637 rNumStr
= rNumStr
.replaceAt(0, 1, rtl::OUStringChar(
638 msfilter::util::bestFitOpenSymbolToMSFont(rNumStr
[0], rChrSet
, sFontName
)));
641 rFontName
= sFontName
;
644 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */