Line data Source code
1 : /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 : /*
3 : * This file is part of the LibreOffice project.
4 : *
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/.
8 : *
9 : * This file incorporates work covered by the following license notice:
10 : *
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 .
18 : */
19 :
20 :
21 : #include <editeng/unolingu.hxx>
22 :
23 : #include <unobaseclass.hxx>
24 : #include <unoflatpara.hxx>
25 :
26 : #include <osl/mutex.hxx>
27 : #include <vcl/svapp.hxx>
28 : #include <com/sun/star/text/TextMarkupType.hpp>
29 : #include <unotextmarkup.hxx>
30 : #include <ndtxt.hxx>
31 : #include <doc.hxx>
32 : #include <docsh.hxx>
33 : #include <viewsh.hxx>
34 : #include <viewimp.hxx>
35 : #include <breakit.hxx>
36 : #include <pam.hxx>
37 : #include <unotextrange.hxx>
38 : #include <pagefrm.hxx>
39 : #include <cntfrm.hxx>
40 : #include <rootfrm.hxx>
41 : #include <poolfmt.hxx>
42 : #include <pagedesc.hxx>
43 : #include <IGrammarContact.hxx>
44 : #include <viewopt.hxx>
45 : #include <comphelper/servicehelper.hxx>
46 :
47 : #include <com/sun/star/lang/XUnoTunnel.hpp>
48 : #include <com/sun/star/text/XTextRange.hpp>
49 :
50 : using namespace ::com::sun::star;
51 :
52 : namespace SwUnoCursorHelper {
53 :
54 : uno::Reference<text::XFlatParagraphIterator>
55 0 : CreateFlatParagraphIterator(SwDoc & rDoc, sal_Int32 const nTextMarkupType,
56 : bool const bAutomatic)
57 : {
58 0 : return new SwXFlatParagraphIterator(rDoc, nTextMarkupType, bAutomatic);
59 : }
60 :
61 : }
62 :
63 : /******************************************************************************
64 : * SwXFlatParagraph
65 : ******************************************************************************/
66 :
67 0 : SwXFlatParagraph::SwXFlatParagraph( SwTxtNode& rTxtNode, OUString aExpandText, const ModelToViewHelper& rMap ) :
68 : SwXTextMarkup( rTxtNode, rMap ),
69 0 : maExpandText( aExpandText )
70 : {
71 0 : }
72 :
73 0 : SwXFlatParagraph::~SwXFlatParagraph()
74 : {
75 0 : }
76 :
77 0 : uno::Sequence< uno::Type > SwXFlatParagraph::getTypes( ) throw(uno::RuntimeException)
78 : {
79 0 : uno::Sequence< uno::Type > aTypes = SwXTextMarkup::getTypes();
80 0 : aTypes.realloc( aTypes.getLength() + 1 );
81 0 : aTypes[aTypes.getLength()-1] = ::getCppuType((uno::Reference< text::XFlatParagraph >*)0);
82 0 : return aTypes;
83 : }
84 :
85 : namespace
86 : {
87 : class theSwXFlatParagraphImplementationId : public rtl::Static< UnoTunnelIdInit, theSwXFlatParagraphImplementationId > {};
88 : }
89 :
90 0 : uno::Sequence< sal_Int8 > SwXFlatParagraph::getImplementationId( ) throw(uno::RuntimeException)
91 : {
92 0 : return theSwXFlatParagraphImplementationId::get().getSeq();
93 : }
94 :
95 0 : uno::Any SAL_CALL SwXFlatParagraph::queryInterface( const uno::Type& rType ) throw(uno::RuntimeException)
96 : {
97 0 : if ( rType == ::getCppuType((uno::Reference< text::XFlatParagraph >*)0) )
98 : {
99 0 : return uno::makeAny( uno::Reference < text::XFlatParagraph >(this) );
100 : }
101 : else
102 0 : return SwXTextMarkup::queryInterface( rType );
103 : }
104 :
105 0 : void SAL_CALL SwXFlatParagraph::acquire() throw()
106 : {
107 0 : SwXTextMarkup::acquire();
108 0 : }
109 :
110 0 : void SAL_CALL SwXFlatParagraph::release() throw()
111 : {
112 0 : SwXTextMarkup::release();
113 0 : }
114 :
115 0 : const SwTxtNode* SwXFlatParagraph::getTxtNode() const
116 : {
117 0 : return mpTxtNode;
118 : }
119 :
120 0 : css::uno::Reference< css::container::XStringKeyMap > SAL_CALL SwXFlatParagraph::getMarkupInfoContainer() throw (css::uno::RuntimeException)
121 : {
122 0 : return SwXTextMarkup::getMarkupInfoContainer();
123 : }
124 :
125 0 : void SAL_CALL SwXFlatParagraph::commitTextRangeMarkup(::sal_Int32 nType, const OUString & aIdentifier, const uno::Reference< text::XTextRange> & xRange,
126 : const css::uno::Reference< css::container::XStringKeyMap > & xMarkupInfoContainer) throw (uno::RuntimeException)
127 : {
128 0 : SolarMutexGuard aGuard;
129 0 : SwXTextMarkup::commitTextRangeMarkup( nType, aIdentifier, xRange, xMarkupInfoContainer );
130 0 : }
131 :
132 0 : void SAL_CALL SwXFlatParagraph::commitStringMarkup(::sal_Int32 nType, const OUString & rIdentifier, ::sal_Int32 nStart, ::sal_Int32 nLength, const css::uno::Reference< css::container::XStringKeyMap > & rxMarkupInfoContainer) throw (css::uno::RuntimeException)
133 : {
134 0 : SolarMutexGuard aGuard;
135 0 : SwXTextMarkup::commitStringMarkup( nType, rIdentifier, nStart, nLength, rxMarkupInfoContainer );
136 0 : }
137 :
138 : // text::XFlatParagraph:
139 0 : OUString SAL_CALL SwXFlatParagraph::getText() throw (uno::RuntimeException)
140 : {
141 0 : return maExpandText;
142 : }
143 :
144 : // text::XFlatParagraph:
145 0 : void SAL_CALL SwXFlatParagraph::setChecked( ::sal_Int32 nType, ::sal_Bool bVal ) throw (uno::RuntimeException)
146 : {
147 0 : SolarMutexGuard aGuard;
148 :
149 0 : if ( mpTxtNode )
150 : {
151 0 : if ( text::TextMarkupType::SPELLCHECK == nType )
152 0 : mpTxtNode->SetWrongDirty( !bVal );
153 0 : else if ( text::TextMarkupType::SMARTTAG == nType )
154 0 : mpTxtNode->SetSmartTagDirty( !bVal );
155 0 : else if( text::TextMarkupType::PROOFREADING == nType )
156 : {
157 0 : mpTxtNode->SetGrammarCheckDirty( !bVal );
158 0 : if( bVal )
159 0 : ::finishGrammarCheck( *mpTxtNode );
160 : }
161 0 : }
162 0 : }
163 :
164 : // text::XFlatParagraph:
165 0 : ::sal_Bool SAL_CALL SwXFlatParagraph::isChecked( ::sal_Int32 nType ) throw (uno::RuntimeException)
166 : {
167 0 : SolarMutexGuard aGuard;
168 0 : if ( mpTxtNode )
169 : {
170 0 : if ( text::TextMarkupType::SPELLCHECK == nType )
171 0 : return mpTxtNode->IsWrongDirty();
172 0 : else if ( text::TextMarkupType::PROOFREADING == nType )
173 0 : return mpTxtNode->IsGrammarCheckDirty();
174 0 : else if ( text::TextMarkupType::SMARTTAG == nType )
175 0 : return mpTxtNode->IsSmartTagDirty();
176 : }
177 :
178 0 : return sal_False;
179 : }
180 :
181 : // text::XFlatParagraph:
182 0 : ::sal_Bool SAL_CALL SwXFlatParagraph::isModified() throw (uno::RuntimeException)
183 : {
184 0 : SolarMutexGuard aGuard;
185 0 : return 0 == mpTxtNode;
186 : }
187 :
188 : // text::XFlatParagraph:
189 0 : lang::Locale SAL_CALL SwXFlatParagraph::getLanguageOfText(::sal_Int32 nPos, ::sal_Int32 nLen)
190 : throw (uno::RuntimeException, lang::IllegalArgumentException)
191 : {
192 0 : SolarMutexGuard aGuard;
193 0 : if (!mpTxtNode)
194 0 : return LanguageTag( LANGUAGE_NONE ).getLocale();
195 :
196 0 : const lang::Locale aLocale( SW_BREAKITER()->GetLocale( mpTxtNode->GetLang( static_cast<sal_uInt16>(nPos), static_cast<sal_uInt16>(nLen) ) ) );
197 0 : return aLocale;
198 : }
199 :
200 : // text::XFlatParagraph:
201 0 : lang::Locale SAL_CALL SwXFlatParagraph::getPrimaryLanguageOfText(::sal_Int32 nPos, ::sal_Int32 nLen)
202 : throw (uno::RuntimeException, lang::IllegalArgumentException)
203 : {
204 0 : SolarMutexGuard aGuard;
205 :
206 0 : if (!mpTxtNode)
207 0 : return LanguageTag( LANGUAGE_NONE ).getLocale();
208 :
209 0 : const lang::Locale aLocale( SW_BREAKITER()->GetLocale( mpTxtNode->GetLang( static_cast<sal_uInt16>(nPos), static_cast<sal_uInt16>(nLen) ) ) );
210 0 : return aLocale;
211 : }
212 :
213 : // text::XFlatParagraph:
214 0 : void SAL_CALL SwXFlatParagraph::changeText(::sal_Int32 nPos, ::sal_Int32 nLen, const OUString & aNewText, const css::uno::Sequence< css::beans::PropertyValue > & aAttributes) throw (css::uno::RuntimeException, css::lang::IllegalArgumentException)
215 : {
216 0 : SolarMutexGuard aGuard;
217 :
218 0 : if ( !mpTxtNode )
219 0 : return;
220 :
221 0 : SwTxtNode* pOldTxtNode = mpTxtNode;
222 :
223 0 : SwPaM aPaM( *mpTxtNode, static_cast<sal_uInt16>(nPos), *mpTxtNode, static_cast<sal_uInt16>(nPos + nLen) );
224 :
225 0 : UnoActionContext aAction( mpTxtNode->GetDoc() );
226 :
227 : const uno::Reference< text::XTextRange > xRange =
228 : SwXTextRange::CreateXTextRange(
229 0 : *mpTxtNode->GetDoc(), *aPaM.GetPoint(), aPaM.GetMark() );
230 0 : uno::Reference< beans::XPropertySet > xPropSet( xRange, uno::UNO_QUERY );
231 0 : if ( xPropSet.is() )
232 : {
233 0 : for ( sal_uInt16 i = 0; i < aAttributes.getLength(); ++i )
234 0 : xPropSet->setPropertyValue( aAttributes[i].Name, aAttributes[i].Value );
235 : }
236 :
237 0 : mpTxtNode = pOldTxtNode; // setPropertyValue() modifies this. We restore the old state.
238 :
239 0 : IDocumentContentOperations* pIDCO = mpTxtNode->getIDocumentContentOperations();
240 0 : pIDCO->ReplaceRange( aPaM, aNewText, false );
241 :
242 0 : mpTxtNode = 0;
243 : }
244 :
245 : // text::XFlatParagraph:
246 0 : void SAL_CALL SwXFlatParagraph::changeAttributes(::sal_Int32 nPos, ::sal_Int32 nLen, const css::uno::Sequence< css::beans::PropertyValue > & aAttributes) throw (css::uno::RuntimeException, css::lang::IllegalArgumentException)
247 : {
248 0 : SolarMutexGuard aGuard;
249 :
250 0 : if ( !mpTxtNode )
251 0 : return;
252 :
253 0 : SwPaM aPaM( *mpTxtNode, static_cast<sal_uInt16>(nPos), *mpTxtNode, static_cast<sal_uInt16>(nPos + nLen) );
254 :
255 0 : UnoActionContext aAction( mpTxtNode->GetDoc() );
256 :
257 : const uno::Reference< text::XTextRange > xRange =
258 : SwXTextRange::CreateXTextRange(
259 0 : *mpTxtNode->GetDoc(), *aPaM.GetPoint(), aPaM.GetMark() );
260 0 : uno::Reference< beans::XPropertySet > xPropSet( xRange, uno::UNO_QUERY );
261 0 : if ( xPropSet.is() )
262 : {
263 0 : for ( sal_uInt16 i = 0; i < aAttributes.getLength(); ++i )
264 0 : xPropSet->setPropertyValue( aAttributes[i].Name, aAttributes[i].Value );
265 : }
266 :
267 0 : mpTxtNode = 0;
268 : }
269 :
270 : // text::XFlatParagraph:
271 0 : css::uno::Sequence< ::sal_Int32 > SAL_CALL SwXFlatParagraph::getLanguagePortions() throw (css::uno::RuntimeException)
272 : {
273 0 : SolarMutexGuard aGuard;
274 0 : return css::uno::Sequence< ::sal_Int32>();
275 : }
276 :
277 : namespace
278 : {
279 : class theSwXFlatParagraphUnoTunnelId : public rtl::Static< UnoTunnelIdInit, theSwXFlatParagraphUnoTunnelId > {};
280 : }
281 :
282 : const uno::Sequence< sal_Int8 >&
283 0 : SwXFlatParagraph::getUnoTunnelId()
284 : {
285 0 : return theSwXFlatParagraphUnoTunnelId::get().getSeq();
286 : }
287 :
288 : sal_Int64 SAL_CALL
289 0 : SwXFlatParagraph::getSomething(
290 : const uno::Sequence< sal_Int8 >& rId)
291 : throw (uno::RuntimeException)
292 : {
293 0 : return sw::UnoTunnelImpl(rId, this);
294 : }
295 :
296 :
297 : /******************************************************************************
298 : * SwXFlatParagraphIterator
299 : ******************************************************************************/
300 :
301 0 : SwXFlatParagraphIterator::SwXFlatParagraphIterator( SwDoc& rDoc, sal_Int32 nType, sal_Bool bAutomatic )
302 : : mpDoc( &rDoc ),
303 : mnType( nType ),
304 : mbAutomatic( bAutomatic ),
305 : mnCurrentNode( 0 ),
306 : mnStartNode( 0 ),
307 0 : mnEndNode( rDoc.GetNodes().Count() ),
308 0 : mbWrapped( sal_False )
309 : {
310 : //mnStartNode = mnCurrentNode = get node from current cursor TODO!
311 :
312 : // register as listener and get notified when document is closed
313 0 : mpDoc->GetPageDescFromPool( RES_POOLPAGE_STANDARD )->Add(this);
314 0 : }
315 :
316 0 : SwXFlatParagraphIterator::~SwXFlatParagraphIterator()
317 : {
318 0 : }
319 :
320 :
321 0 : void SwXFlatParagraphIterator::Modify( const SfxPoolItem* pOld, const SfxPoolItem *pNew )
322 : {
323 0 : ClientModify( this, pOld, pNew );
324 : // check if document gets closed...
325 0 : if(!GetRegisteredIn())
326 : {
327 0 : SolarMutexGuard aGuard;
328 0 : mpDoc = 0;
329 : }
330 0 : }
331 :
332 :
333 0 : uno::Reference< text::XFlatParagraph > SwXFlatParagraphIterator::getFirstPara()
334 : throw( uno::RuntimeException )
335 : {
336 0 : return getNextPara(); // TODO
337 : }
338 :
339 0 : uno::Reference< text::XFlatParagraph > SwXFlatParagraphIterator::getNextPara()
340 : throw( uno::RuntimeException )
341 : {
342 0 : SolarMutexGuard aGuard;
343 :
344 0 : uno::Reference< text::XFlatParagraph > xRet;
345 0 : if (!mpDoc)
346 0 : return xRet;
347 :
348 0 : SwTxtNode* pRet = 0;
349 0 : if ( mbAutomatic )
350 : {
351 0 : ViewShell* pViewShell = 0;
352 0 : mpDoc->GetEditShell( &pViewShell );
353 :
354 0 : SwPageFrm* pCurrentPage = pViewShell ? pViewShell->Imp()->GetFirstVisPage() : 0;
355 0 : SwPageFrm* pStartPage = pCurrentPage;
356 0 : SwPageFrm* pStopPage = 0;
357 :
358 0 : while ( pCurrentPage != pStopPage )
359 : {
360 0 : if (mnType != text::TextMarkupType::SPELLCHECK || pCurrentPage->IsInvalidSpelling() )
361 : {
362 : // this method is supposed to return an empty paragraph in case Online Checking is disabled
363 0 : if ( ( mnType == text::TextMarkupType::PROOFREADING || mnType == text::TextMarkupType::SPELLCHECK )
364 0 : && !pViewShell->GetViewOptions()->IsOnlineSpell() )
365 0 : return xRet;
366 :
367 : // search for invalid content:
368 0 : SwCntntFrm* pCnt = pCurrentPage->ContainsCntnt();
369 :
370 0 : while( pCnt && pCurrentPage->IsAnLower( pCnt ) )
371 : {
372 0 : SwTxtNode* pTxtNode = dynamic_cast<SwTxtNode*>( pCnt->GetNode()->GetTxtNode() );
373 :
374 0 : if ( pTxtNode &&
375 0 : ((mnType == text::TextMarkupType::SPELLCHECK &&
376 0 : pTxtNode->IsWrongDirty()) ||
377 0 : (mnType == text::TextMarkupType::PROOFREADING &&
378 0 : pTxtNode->IsGrammarCheckDirty())) )
379 : {
380 0 : pRet = pTxtNode;
381 0 : break;
382 : }
383 :
384 0 : pCnt = pCnt->GetNextCntntFrm();
385 : }
386 : }
387 :
388 0 : if ( pRet )
389 0 : break;
390 :
391 : // if there is no invalid text node on the current page,
392 : // we validate the page
393 0 : pCurrentPage->ValidateSpelling();
394 :
395 : // proceed with next page, wrap at end of document if required:
396 0 : pCurrentPage = static_cast<SwPageFrm*>(pCurrentPage->GetNext());
397 :
398 0 : if ( !pCurrentPage && !pStopPage )
399 : {
400 0 : pStopPage = pStartPage;
401 0 : pCurrentPage = static_cast<SwPageFrm*>(pViewShell->GetLayout()->Lower());
402 : }
403 : }
404 : }
405 : else // non-automatic checking
406 : {
407 0 : const SwNodes& rNodes = mpDoc->GetNodes();
408 0 : const sal_uLong nMaxNodes = rNodes.Count();
409 :
410 0 : while ( mnCurrentNode < mnEndNode && mnCurrentNode < nMaxNodes )
411 : {
412 0 : SwNode* pNd = rNodes[ mnCurrentNode ];
413 :
414 0 : ++mnCurrentNode;
415 :
416 0 : pRet = dynamic_cast<SwTxtNode*>(pNd);
417 0 : if ( pRet )
418 0 : break;
419 :
420 0 : if ( mnCurrentNode == mnEndNode && !mbWrapped )
421 : {
422 0 : mnCurrentNode = 0;
423 0 : mnEndNode = mnStartNode;
424 : }
425 : }
426 : }
427 :
428 0 : if ( pRet )
429 : {
430 : // Expand the string:
431 0 : const ModelToViewHelper aConversionMap(*pRet);
432 0 : OUString aExpandText = aConversionMap.getViewText();
433 :
434 0 : xRet = new SwXFlatParagraph( *pRet, aExpandText, aConversionMap );
435 : // keep hard references...
436 0 : m_aFlatParaList.insert( xRet );
437 : }
438 :
439 0 : return xRet;
440 : }
441 :
442 0 : uno::Reference< text::XFlatParagraph > SwXFlatParagraphIterator::getLastPara()
443 : throw( uno::RuntimeException )
444 : {
445 0 : return getNextPara();
446 : }
447 :
448 0 : uno::Reference< text::XFlatParagraph > SwXFlatParagraphIterator::getParaAfter(const uno::Reference< text::XFlatParagraph > & xPara)
449 : throw ( uno::RuntimeException, lang::IllegalArgumentException )
450 : {
451 0 : SolarMutexGuard aGuard;
452 :
453 0 : uno::Reference< text::XFlatParagraph > xRet;
454 0 : if (!mpDoc)
455 0 : return xRet;
456 :
457 0 : const uno::Reference<lang::XUnoTunnel> xFPTunnel(xPara, uno::UNO_QUERY);
458 : OSL_ASSERT(xFPTunnel.is());
459 0 : SwXFlatParagraph* const pFlatParagraph(sw::UnoTunnelGetImplementation<SwXFlatParagraph>(xFPTunnel));
460 :
461 0 : if ( !pFlatParagraph )
462 0 : return xRet;
463 :
464 0 : const SwTxtNode* pCurrentNode = pFlatParagraph->getTxtNode();
465 :
466 0 : if ( !pCurrentNode )
467 0 : return xRet;
468 :
469 0 : SwTxtNode* pNextTxtNode = 0;
470 0 : const SwNodes& rNodes = pCurrentNode->GetDoc()->GetNodes();
471 :
472 0 : for( sal_uLong nCurrentNode = pCurrentNode->GetIndex() + 1; nCurrentNode < rNodes.Count(); ++nCurrentNode )
473 : {
474 0 : SwNode* pNd = rNodes[ nCurrentNode ];
475 0 : pNextTxtNode = dynamic_cast<SwTxtNode*>(pNd);
476 0 : if ( pNextTxtNode )
477 0 : break;
478 : }
479 :
480 0 : if ( pNextTxtNode )
481 : {
482 : // Expand the string:
483 0 : const ModelToViewHelper aConversionMap(*pNextTxtNode);
484 0 : OUString aExpandText = aConversionMap.getViewText();
485 :
486 0 : xRet = new SwXFlatParagraph( *pNextTxtNode, aExpandText, aConversionMap );
487 : // keep hard references...
488 0 : m_aFlatParaList.insert( xRet );
489 : }
490 :
491 0 : return xRet;
492 : }
493 :
494 0 : uno::Reference< text::XFlatParagraph > SwXFlatParagraphIterator::getParaBefore(const uno::Reference< text::XFlatParagraph > & xPara )
495 : throw ( uno::RuntimeException, lang::IllegalArgumentException )
496 : {
497 0 : SolarMutexGuard aGuard;
498 :
499 0 : uno::Reference< text::XFlatParagraph > xRet;
500 0 : if (!mpDoc)
501 0 : return xRet;
502 :
503 0 : const uno::Reference<lang::XUnoTunnel> xFPTunnel(xPara, uno::UNO_QUERY);
504 : OSL_ASSERT(xFPTunnel.is());
505 0 : SwXFlatParagraph* const pFlatParagraph(sw::UnoTunnelGetImplementation<SwXFlatParagraph>(xFPTunnel));
506 :
507 0 : if ( !pFlatParagraph )
508 0 : return xRet;
509 :
510 0 : const SwTxtNode* pCurrentNode = pFlatParagraph->getTxtNode();
511 :
512 0 : if ( !pCurrentNode )
513 0 : return xRet;
514 :
515 0 : SwTxtNode* pPrevTxtNode = 0;
516 0 : const SwNodes& rNodes = pCurrentNode->GetDoc()->GetNodes();
517 :
518 0 : for( sal_uLong nCurrentNode = pCurrentNode->GetIndex() - 1; nCurrentNode > 0; --nCurrentNode )
519 : {
520 0 : SwNode* pNd = rNodes[ nCurrentNode ];
521 0 : pPrevTxtNode = dynamic_cast<SwTxtNode*>(pNd);
522 0 : if ( pPrevTxtNode )
523 0 : break;
524 : }
525 :
526 0 : if ( pPrevTxtNode )
527 : {
528 : // Expand the string:
529 0 : const ModelToViewHelper aConversionMap(*pPrevTxtNode);
530 0 : OUString aExpandText = aConversionMap.getViewText();
531 :
532 0 : xRet = new SwXFlatParagraph( *pPrevTxtNode, aExpandText, aConversionMap );
533 : // keep hard references...
534 0 : m_aFlatParaList.insert( xRet );
535 : }
536 :
537 0 : return xRet;
538 99 : }
539 :
540 : /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|