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 : #include <hintids.hxx>
21 : #include <svl/macitem.hxx>
22 : #include <sfx2/frame.hxx>
23 : #include <vcl/msgbox.hxx>
24 : #include <svl/urihelper.hxx>
25 : #include <svl/eitem.hxx>
26 : #include <svl/stritem.hxx>
27 : #include <sfx2/docfile.hxx>
28 : #include <sfx2/fcontnr.hxx>
29 : #include <sfx2/dispatch.hxx>
30 : #include <sfx2/linkmgr.hxx>
31 : #include <fmtinfmt.hxx>
32 : #include <frmatr.hxx>
33 : #include <swtypes.hxx>
34 : #include <wrtsh.hxx>
35 : #include <docsh.hxx>
36 : #include <fldbas.hxx>
37 : #include <expfld.hxx>
38 : #include <ddefld.hxx>
39 : #include <docufld.hxx>
40 : #include <reffld.hxx>
41 : #include <swundo.hxx>
42 : #include <doc.hxx>
43 : #include <IDocumentUndoRedo.hxx>
44 : #include <viewopt.hxx>
45 : #include <frmfmt.hxx>
46 : #include <fmtfld.hxx>
47 : #include <swtable.hxx>
48 : #include <mdiexp.hxx>
49 : #include <view.hxx>
50 : #include <swevent.hxx>
51 : #include <poolfmt.hxx>
52 : #include <section.hxx>
53 : #include <navicont.hxx>
54 : #include <navipi.hxx>
55 : #include <crsskip.hxx>
56 : #include <txtinet.hxx>
57 : #include <cmdid.h>
58 : #include <wrtsh.hrc>
59 : #include "swabstdlg.hxx"
60 : #include "fldui.hrc"
61 : #include <SwRewriter.hxx>
62 :
63 : #include <com/sun/star/document/XDocumentProperties.hpp>
64 : #include <com/sun/star/document/XDocumentPropertiesSupplier.hpp>
65 :
66 : #include <xmloff/odffields.hxx>
67 : #include <boost/scoped_ptr.hpp>
68 :
69 : #include <LibreOfficeKit/LibreOfficeKitEnums.h>
70 :
71 1 : void SwWrtShell::Insert(SwField &rField)
72 : {
73 1 : ResetCursorStack();
74 1 : if(!CanInsert())
75 1 : return;
76 1 : StartAllAction();
77 :
78 1 : SwRewriter aRewriter;
79 1 : aRewriter.AddRule(UndoArg1, rField.GetDescription());
80 :
81 1 : StartUndo(UNDO_INSERT, &aRewriter);
82 :
83 1 : bool bDeleted = false;
84 2 : boost::scoped_ptr<SwPaM> pAnnotationTextRange;
85 1 : if ( HasSelection() )
86 : {
87 0 : if ( rField.GetTyp()->Which() == RES_POSTITFLD )
88 : {
89 : // for annotation fields:
90 : // - keep the current selection in order to create a corresponding annotation mark
91 : // - collapse cursor to its end
92 0 : if ( IsTableMode() )
93 : {
94 0 : GetTableCrs()->Normalize( false );
95 0 : const SwPosition rStartPos( *(GetTableCrs()->GetMark()->nNode.GetNode().GetContentNode()), 0 );
96 0 : KillPams();
97 0 : if ( !IsEndOfPara() )
98 : {
99 0 : EndPara();
100 : }
101 0 : const SwPosition rEndPos( *GetCurrentShellCursor().GetPoint() );
102 0 : pAnnotationTextRange.reset(new SwPaM( rStartPos, rEndPos ));
103 : }
104 : else
105 : {
106 0 : NormalizePam( false );
107 0 : const SwPaM& rCurrPaM = GetCurrentShellCursor();
108 0 : pAnnotationTextRange.reset(new SwPaM( *rCurrPaM.GetPoint(), *rCurrPaM.GetMark() ));
109 0 : ClearMark();
110 : }
111 : }
112 : else
113 : {
114 0 : bDeleted = DelRight() != 0;
115 : }
116 : }
117 :
118 1 : SwEditShell::Insert2(rField, bDeleted);
119 :
120 1 : if ( pAnnotationTextRange )
121 : {
122 0 : if ( GetDoc() != NULL )
123 : {
124 0 : IDocumentMarkAccess* pMarksAccess = GetDoc()->getIDocumentMarkAccess();
125 0 : pMarksAccess->makeAnnotationMark( *pAnnotationTextRange, OUString() );
126 : }
127 0 : pAnnotationTextRange.reset();
128 : }
129 :
130 1 : EndUndo();
131 2 : EndAllAction();
132 : }
133 :
134 : // Start the field update
135 :
136 0 : void SwWrtShell::UpdateInputFields( SwInputFieldList* pLst )
137 : {
138 : // Go through the list of fields and updating
139 0 : SwInputFieldList* pTmp = pLst;
140 0 : if( !pTmp )
141 0 : pTmp = new SwInputFieldList( this );
142 :
143 0 : const size_t nCnt = pTmp->Count();
144 0 : if(nCnt)
145 : {
146 0 : pTmp->PushCrsr();
147 :
148 0 : bool bCancel = false;
149 0 : OString aDlgPos;
150 0 : for( size_t i = 0; i < nCnt && !bCancel; ++i )
151 : {
152 0 : pTmp->GotoFieldPos( i );
153 0 : SwField* pField = pTmp->GetField( i );
154 0 : if(pField->GetTyp()->Which() == RES_DROPDOWN)
155 0 : bCancel = StartDropDownFieldDlg( pField, true, &aDlgPos );
156 : else
157 0 : bCancel = StartInputFieldDlg( pField, true, 0, &aDlgPos);
158 :
159 0 : if (!bCancel)
160 : {
161 : // Otherwise update error at multi-selection:
162 0 : pTmp->GetField( i )->GetTyp()->UpdateFields();
163 : }
164 : }
165 0 : pTmp->PopCrsr();
166 : }
167 :
168 0 : if( !pLst )
169 0 : delete pTmp;
170 0 : }
171 :
172 : // Listener class: will close InputField dialog if input field(s)
173 : // is(are) deleted (for instance, by an extension) after the dialog shows up.
174 : // Otherwise, the for loop in SwWrtShell::UpdateInputFields will crash when doing:
175 : // 'pTmp->GetField( i )->GetTyp()->UpdateFields();'
176 : // on a deleted field.
177 : class FieldDeletionModify : public SwModify
178 : {
179 : public:
180 0 : FieldDeletionModify(AbstractFieldInputDlg* pInputFieldDlg, SwField* pField)
181 : : mpInputFieldDlg(pInputFieldDlg)
182 0 : , mpFormatField(NULL)
183 : {
184 0 : SwInputField *const pInputField(dynamic_cast<SwInputField*>(pField));
185 0 : SwSetExpField *const pSetExpField(dynamic_cast<SwSetExpField*>(pField));
186 :
187 0 : if (pInputField && pInputField->GetFormatField())
188 : {
189 0 : mpFormatField = pInputField->GetFormatField();
190 : }
191 0 : else if (pSetExpField && pSetExpField->GetFormatField())
192 : {
193 0 : mpFormatField = pSetExpField->GetFormatField();
194 : }
195 :
196 : // Register for possible field deletion while dialog is open
197 0 : if (mpFormatField)
198 0 : mpFormatField->Add(this);
199 0 : }
200 :
201 0 : virtual ~FieldDeletionModify()
202 0 : {
203 0 : if (mpFormatField)
204 : {
205 : // Dialog closed, remove modification listener
206 0 : mpFormatField->Remove(this);
207 : }
208 0 : }
209 :
210 0 : void Modify( const SfxPoolItem* pOld, const SfxPoolItem *) SAL_OVERRIDE
211 : {
212 : // Input field has been deleted: better to close the dialog
213 0 : if (pOld)
214 : {
215 0 : switch (pOld->Which())
216 : {
217 : case RES_REMOVE_UNO_OBJECT:
218 : case RES_OBJECTDYING:
219 0 : mpFormatField = NULL;
220 0 : mpInputFieldDlg->EndDialog(RET_CANCEL);
221 0 : break;
222 : }
223 : }
224 0 : }
225 : private:
226 : AbstractFieldInputDlg* mpInputFieldDlg;
227 : SwFormatField* mpFormatField;
228 : };
229 :
230 : // Start input dialog for a specific field
231 0 : bool SwWrtShell::StartInputFieldDlg( SwField* pField, bool bNextButton,
232 : vcl::Window* pParentWin, OString* pWindowState )
233 : {
234 :
235 0 : SwAbstractDialogFactory* pFact = SwAbstractDialogFactory::Create();
236 : OSL_ENSURE(pFact, "Dialog creation failed!");
237 0 : boost::scoped_ptr<AbstractFieldInputDlg> pDlg(pFact->CreateFieldInputDlg(pParentWin, *this, pField, bNextButton));
238 : OSL_ENSURE(pDlg, "Dialog creation failed!");
239 0 : if(pWindowState && !pWindowState->isEmpty())
240 0 : pDlg->SetWindowState(*pWindowState);
241 :
242 : bool bRet;
243 :
244 : {
245 0 : FieldDeletionModify aModify(pDlg.get(), pField);
246 0 : bRet = RET_CANCEL == pDlg->Execute();
247 : }
248 :
249 0 : if(pWindowState)
250 0 : *pWindowState = pDlg->GetWindowState();
251 :
252 0 : pDlg.reset();
253 0 : GetWin()->Update();
254 0 : return bRet;
255 : }
256 :
257 0 : bool SwWrtShell::StartDropDownFieldDlg(SwField* pField, bool bNextButton, OString* pWindowState)
258 : {
259 0 : SwAbstractDialogFactory* pFact = SwAbstractDialogFactory::Create();
260 : OSL_ENSURE(pFact, "SwAbstractDialogFactory fail!");
261 :
262 0 : boost::scoped_ptr<AbstractDropDownFieldDialog> pDlg(pFact->CreateDropDownFieldDialog(NULL, *this, pField, bNextButton));
263 : OSL_ENSURE(pDlg, "Dialog creation failed!");
264 0 : if(pWindowState && !pWindowState->isEmpty())
265 0 : pDlg->SetWindowState(*pWindowState);
266 0 : const short nRet = pDlg->Execute();
267 0 : if(pWindowState)
268 0 : *pWindowState = pDlg->GetWindowState();
269 0 : pDlg.reset();
270 0 : bool bRet = RET_CANCEL == nRet;
271 0 : GetWin()->Update();
272 0 : if(RET_YES == nRet)
273 : {
274 0 : GetView().GetViewFrame()->GetDispatcher()->Execute(FN_EDIT_FIELD, SfxCallMode::SYNCHRON);
275 : }
276 0 : return bRet;
277 : }
278 :
279 : // Insert directory - remove selection
280 :
281 0 : void SwWrtShell::InsertTableOf(const SwTOXBase& rTOX, const SfxItemSet* pSet)
282 : {
283 0 : if(!CanInsert())
284 0 : return;
285 :
286 0 : if(HasSelection())
287 0 : DelRight();
288 :
289 0 : SwEditShell::InsertTableOf(rTOX, pSet);
290 : }
291 :
292 : // Update directory - remove selection
293 :
294 0 : bool SwWrtShell::UpdateTableOf(const SwTOXBase& rTOX, const SfxItemSet* pSet)
295 : {
296 0 : bool bResult = false;
297 :
298 0 : if(CanInsert())
299 : {
300 0 : bResult = SwEditShell::UpdateTableOf(rTOX, pSet);
301 :
302 0 : if (pSet == NULL)
303 : {
304 0 : SwDoc *const pDoc_ = GetDoc();
305 0 : if (pDoc_)
306 : {
307 0 : pDoc_->GetIDocumentUndoRedo().DelAllUndoObj();
308 : }
309 : }
310 : }
311 :
312 0 : return bResult;
313 : }
314 :
315 : // handler for click on the field given as parameter.
316 : // the cursor is positioned on the field.
317 :
318 0 : void SwWrtShell::ClickToField( const SwField& rField )
319 : {
320 : // cross reference field must not be selected because it moves the cursor
321 0 : if (RES_GETREFFLD != rField.GetTyp()->Which())
322 : {
323 0 : StartAllAction();
324 0 : Right( CRSR_SKIP_CHARS, true, 1, false ); // Select the field.
325 0 : NormalizePam();
326 0 : EndAllAction();
327 : }
328 :
329 0 : m_bIsInClickToEdit = true;
330 0 : switch( rField.GetTyp()->Which() )
331 : {
332 : case RES_JUMPEDITFLD:
333 : {
334 0 : sal_uInt16 nSlotId = 0;
335 0 : switch( rField.GetFormat() )
336 : {
337 : case JE_FMT_TABLE:
338 0 : nSlotId = FN_INSERT_TABLE;
339 0 : break;
340 :
341 : case JE_FMT_FRAME:
342 0 : nSlotId = FN_INSERT_FRAME;
343 0 : break;
344 :
345 0 : case JE_FMT_GRAPHIC: nSlotId = SID_INSERT_GRAPHIC; break;
346 0 : case JE_FMT_OLE: nSlotId = SID_INSERT_OBJECT; break;
347 :
348 : }
349 :
350 0 : if( nSlotId )
351 : {
352 0 : StartUndo( UNDO_START );
353 : //#97295# immediately select the right shell
354 0 : GetView().StopShellTimer();
355 0 : GetView().GetViewFrame()->GetDispatcher()->Execute( nSlotId,
356 0 : SfxCallMode::SYNCHRON|SfxCallMode::RECORD );
357 0 : EndUndo( UNDO_END );
358 : }
359 : }
360 0 : break;
361 :
362 : case RES_MACROFLD:
363 : {
364 0 : const SwMacroField *pField = static_cast<const SwMacroField*>(&rField);
365 0 : const OUString sText( rField.GetPar2() );
366 0 : OUString sRet( sText );
367 0 : ExecMacro( pField->GetSvxMacro(), &sRet );
368 :
369 : // return value changed?
370 0 : if( sRet != sText )
371 : {
372 0 : StartAllAction();
373 0 : const_cast<SwField&>(rField).SetPar2( sRet );
374 0 : rField.GetTyp()->UpdateFields();
375 0 : EndAllAction();
376 0 : }
377 : }
378 0 : break;
379 :
380 : case RES_GETREFFLD:
381 0 : StartAllAction();
382 : SwCrsrShell::GotoRefMark( static_cast<const SwGetRefField&>(rField).GetSetRefName(),
383 0 : static_cast<const SwGetRefField&>(rField).GetSubType(),
384 0 : static_cast<const SwGetRefField&>(rField).GetSeqNo() );
385 0 : EndAllAction();
386 0 : break;
387 :
388 : case RES_INPUTFLD:
389 : {
390 0 : const SwInputField* pInputField = dynamic_cast<const SwInputField*>(&rField);
391 0 : if ( pInputField == NULL )
392 : {
393 0 : StartInputFieldDlg( const_cast<SwField*>(&rField), false );
394 : }
395 : }
396 0 : break;
397 :
398 : case RES_SETEXPFLD:
399 0 : if( static_cast<const SwSetExpField&>(rField).GetInputFlag() )
400 0 : StartInputFieldDlg( const_cast<SwField*>(&rField), false );
401 0 : break;
402 : case RES_DROPDOWN :
403 0 : StartDropDownFieldDlg( const_cast<SwField*>(&rField), false );
404 0 : break;
405 : default:
406 : SAL_WARN_IF(rField.IsClickable(), "sw", "unhandled clickable field!");
407 : }
408 :
409 0 : m_bIsInClickToEdit = false;
410 0 : }
411 :
412 0 : void SwWrtShell::ClickToINetAttr( const SwFormatINetFormat& rItem, sal_uInt16 nFilter )
413 : {
414 0 : if( rItem.GetValue().isEmpty() )
415 0 : return ;
416 :
417 0 : m_bIsInClickToEdit = true;
418 :
419 : // At first run the possibly set ObjectSelect Macro
420 0 : const SvxMacro* pMac = rItem.GetMacro( SFX_EVENT_MOUSECLICK_OBJECT );
421 0 : if( pMac )
422 : {
423 0 : SwCallMouseEvent aCallEvent;
424 0 : aCallEvent.Set( &rItem );
425 0 : GetDoc()->CallEvent( SFX_EVENT_MOUSECLICK_OBJECT, aCallEvent, false );
426 : }
427 :
428 : // So that the implementation of templates is displayed immediately
429 0 : ::LoadURL( *this, rItem.GetValue(), nFilter, rItem.GetTargetFrame() );
430 0 : const SwTextINetFormat* pTextAttr = rItem.GetTextINetFormat();
431 0 : if( pTextAttr )
432 : {
433 0 : const_cast<SwTextINetFormat*>(pTextAttr)->SetVisited( true );
434 0 : const_cast<SwTextINetFormat*>(pTextAttr)->SetVisitedValid( true );
435 : }
436 :
437 0 : m_bIsInClickToEdit = false;
438 : }
439 :
440 0 : bool SwWrtShell::ClickToINetGrf( const Point& rDocPt, sal_uInt16 nFilter )
441 : {
442 0 : bool bRet = false;
443 0 : OUString sURL;
444 0 : OUString sTargetFrameName;
445 0 : const SwFrameFormat* pFnd = IsURLGrfAtPos( rDocPt, &sURL, &sTargetFrameName );
446 0 : if( pFnd && !sURL.isEmpty() )
447 : {
448 0 : bRet = true;
449 : // At first run the possibly set ObjectSelect Macro
450 0 : const SvxMacro* pMac = &pFnd->GetMacro().GetMacro( SFX_EVENT_MOUSECLICK_OBJECT );
451 0 : if( pMac )
452 : {
453 0 : SwCallMouseEvent aCallEvent;
454 0 : aCallEvent.Set( EVENT_OBJECT_URLITEM, pFnd );
455 0 : GetDoc()->CallEvent( SFX_EVENT_MOUSECLICK_OBJECT, aCallEvent, false );
456 : }
457 :
458 0 : ::LoadURL(*this, sURL, nFilter, sTargetFrameName);
459 : }
460 0 : return bRet;
461 : }
462 :
463 0 : void LoadURL( SwViewShell& rVSh, const OUString& rURL, sal_uInt16 nFilter,
464 : const OUString& rTargetFrameName )
465 : {
466 : OSL_ENSURE( !rURL.isEmpty(), "what should be loaded here?" );
467 0 : if( rURL.isEmpty() )
468 0 : return ;
469 :
470 : // The shell could be 0 also!!!!!
471 0 : if ( !rVSh.ISA(SwCrsrShell) )
472 0 : return;
473 :
474 : // We are doing tiledRendering, let the client handles the URL loading.
475 0 : if (rVSh.isTiledRendering())
476 : {
477 0 : rVSh.libreOfficeKitCallback(LOK_CALLBACK_HYPERLINK_CLICKED, rURL.toUtf8().getStr());
478 0 : return;
479 : }
480 :
481 : //A CrsrShell is always a WrtShell
482 0 : SwWrtShell &rSh = static_cast<SwWrtShell&>(rVSh);
483 :
484 0 : SwDocShell* pDShell = rSh.GetView().GetDocShell();
485 : OSL_ENSURE( pDShell, "No DocShell?!");
486 0 : OUString sTargetFrame(rTargetFrameName);
487 0 : if (sTargetFrame.isEmpty() && pDShell)
488 : {
489 : using namespace ::com::sun::star;
490 : uno::Reference<document::XDocumentPropertiesSupplier> xDPS(
491 0 : pDShell->GetModel(), uno::UNO_QUERY_THROW);
492 : uno::Reference<document::XDocumentProperties> xDocProps
493 0 : = xDPS->getDocumentProperties();
494 0 : sTargetFrame = xDocProps->getDefaultTarget();
495 : }
496 :
497 0 : OUString sReferer;
498 0 : if( pDShell && pDShell->GetMedium() )
499 0 : sReferer = pDShell->GetMedium()->GetName();
500 0 : SfxViewFrame* pViewFrm = rSh.GetView().GetViewFrame();
501 0 : SfxFrameItem aView( SID_DOCFRAME, pViewFrm );
502 0 : SfxStringItem aName( SID_FILE_NAME, rURL );
503 0 : SfxStringItem aTargetFrameName( SID_TARGETNAME, sTargetFrame );
504 0 : SfxStringItem aReferer( SID_REFERER, sReferer );
505 :
506 0 : SfxBoolItem aNewView( SID_OPEN_NEW_VIEW, false );
507 : //#39076# Silent can be removed accordingly to SFX.
508 0 : SfxBoolItem aBrowse( SID_BROWSE, true );
509 :
510 0 : if( nFilter & URLLOAD_NEWVIEW )
511 0 : aTargetFrameName.SetValue( OUString("_blank") );
512 :
513 : const SfxPoolItem* aArr[] = {
514 : &aName,
515 : &aNewView, /*&aSilent,*/
516 : &aReferer,
517 : &aView, &aTargetFrameName,
518 : &aBrowse,
519 : 0L
520 0 : };
521 :
522 : pViewFrm->GetDispatcher()->GetBindings()->Execute( SID_OPENDOC, aArr, 0,
523 0 : SfxCallMode::ASYNCHRON|SfxCallMode::RECORD );
524 : }
525 :
526 0 : void SwWrtShell::NavigatorPaste( const NaviContentBookmark& rBkmk,
527 : const sal_uInt16 nAction )
528 : {
529 0 : if( EXCHG_IN_ACTION_COPY == nAction )
530 : {
531 : // Insert
532 0 : OUString sURL = rBkmk.GetURL();
533 : // Is this is a jump within the current Doc?
534 0 : const SwDocShell* pDocShell = GetView().GetDocShell();
535 0 : if(pDocShell->HasName())
536 : {
537 0 : const OUString rName = pDocShell->GetMedium()->GetURLObject().GetURLNoMark();
538 :
539 0 : if (sURL.startsWith(rName))
540 : {
541 0 : if (sURL.getLength()>rName.getLength())
542 : {
543 0 : sURL = sURL.copy(rName.getLength());
544 : }
545 : else
546 : {
547 0 : sURL.clear();
548 : }
549 0 : }
550 : }
551 0 : SwFormatINetFormat aFormat( sURL, OUString() );
552 0 : InsertURL( aFormat, rBkmk.GetDescription() );
553 : }
554 : else
555 : {
556 0 : SwSectionData aSection( FILE_LINK_SECTION, GetUniqueSectionName() );
557 0 : OUString aLinkFile( rBkmk.GetURL().getToken(0, '#') );
558 0 : aLinkFile += OUString(sfx2::cTokenSeparator);
559 0 : aLinkFile += OUString(sfx2::cTokenSeparator);
560 0 : aLinkFile += rBkmk.GetURL().getToken(1, '#');
561 0 : aSection.SetLinkFileName( aLinkFile );
562 0 : aSection.SetProtectFlag( true );
563 0 : const SwSection* pIns = InsertSection( aSection );
564 0 : if( EXCHG_IN_ACTION_MOVE == nAction && pIns )
565 : {
566 0 : aSection = SwSectionData(*pIns);
567 0 : aSection.SetLinkFileName( OUString() );
568 0 : aSection.SetType( CONTENT_SECTION );
569 0 : aSection.SetProtectFlag( false );
570 :
571 : // the update of content from linked section at time delete
572 : // the undostack. Then the change of the section dont create
573 : // any undoobject. - BUG 69145
574 0 : bool bDoesUndo = DoesUndo();
575 0 : SwUndoId nLastUndoId(UNDO_EMPTY);
576 0 : if (GetLastUndoInfo(0, & nLastUndoId))
577 : {
578 0 : if (UNDO_INSSECTION != nLastUndoId)
579 : {
580 0 : DoUndo(false);
581 : }
582 : }
583 0 : UpdateSection( GetSectionFormatPos( *pIns->GetFormat() ), aSection );
584 0 : DoUndo( bDoesUndo );
585 0 : }
586 : }
587 177 : }
588 :
589 : /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|