Coverage Report

Created: 2025-12-31 10:39

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/svx/source/diagram/datamodel.cxx
Line
Count
Source
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 <unordered_set>
21
#include <algorithm>
22
#include <fstream>
23
24
#include <svx/diagram/datamodel.hxx>
25
#include <svx/svdoutl.hxx>
26
#include <comphelper/xmltools.hxx>
27
#include <sal/log.hxx>
28
#include <utility>
29
30
namespace svx::diagram {
31
32
Connection::Connection()
33
5.94k
: mnXMLType( XML_none )
34
5.94k
, mnSourceOrder( 0 )
35
5.94k
, mnDestOrder( 0 )
36
5.94k
{
37
5.94k
}
38
39
Point::Point()
40
6.90k
: msTextBody(std::make_shared< TextBody >())
41
6.90k
, msPointStylePtr(std::make_shared< PointStyle >())
42
6.90k
, mnXMLType(XML_none)
43
6.90k
, mnMaxChildren(-1)
44
6.90k
, mnPreferredChildren(-1)
45
6.90k
, mnDirection(XML_norm)
46
6.90k
, mnResizeHandles(XML_rel)
47
6.90k
, mnCustomAngle(-1)
48
6.90k
, mnPercentageNeighbourWidth(-1)
49
6.90k
, mnPercentageNeighbourHeight(-1)
50
6.90k
, mnPercentageOwnWidth(-1)
51
6.90k
, mnPercentageOwnHeight(-1)
52
6.90k
, mnIncludeAngleScale(-1)
53
6.90k
, mnRadiusScale(-1)
54
6.90k
, mnWidthScale(-1)
55
6.90k
, mnHeightScale(-1)
56
6.90k
, mnWidthOverride(-1)
57
6.90k
, mnHeightOverride(-1)
58
6.90k
, mnLayoutStyleCount(-1)
59
6.90k
, mnLayoutStyleIndex(-1)
60
6.90k
, mbOrgChartEnabled(false)
61
6.90k
, mbBulletEnabled(false)
62
6.90k
, mbCoherent3DOffset(false)
63
6.90k
, mbCustomHorizontalFlip(false)
64
6.90k
, mbCustomVerticalFlip(false)
65
6.90k
, mbCustomText(false)
66
6.90k
, mbIsPlaceholder(false)
67
6.90k
{
68
6.90k
}
69
70
DiagramData::DiagramData()
71
958
{
72
958
}
73
74
DiagramData::~DiagramData()
75
958
{
76
958
}
77
78
const Point* DiagramData::getRootPoint() const
79
143
{
80
143
    for (const auto & aCurrPoint : maPoints)
81
94
        if (aCurrPoint.mnXMLType == TypeConstant::XML_doc)
82
94
            return &aCurrPoint;
83
84
49
    SAL_WARN("svx.diagram", "No root point");
85
49
    return nullptr;
86
49
}
87
88
OUString DiagramData::getString() const
89
0
{
90
0
    OUStringBuffer aBuf;
91
0
    const Point* pPoint = getRootPoint();
92
0
    getChildrenString(aBuf, pPoint, 0);
93
0
    return aBuf.makeStringAndClear();
94
0
}
95
96
bool DiagramData::removeNode(const OUString& rNodeId)
97
0
{
98
    // check if it doesn't have children
99
0
    for (const auto& aCxn : maConnections)
100
0
        if (aCxn.mnXMLType == TypeConstant::XML_parOf && aCxn.msSourceId == rNodeId)
101
0
        {
102
0
            SAL_WARN("svx.diagram", "Node has children - can't be removed");
103
0
            return false;
104
0
        }
105
106
0
    Connection aParCxn;
107
0
    for (const auto& aCxn : maConnections)
108
0
        if (aCxn.mnXMLType == TypeConstant::XML_parOf && aCxn.msDestId == rNodeId)
109
0
            aParCxn = aCxn;
110
111
0
    std::unordered_set<OUString> aIdsToRemove;
112
0
    aIdsToRemove.insert(rNodeId);
113
0
    if (!aParCxn.msParTransId.isEmpty())
114
0
        aIdsToRemove.insert(aParCxn.msParTransId);
115
0
    if (!aParCxn.msSibTransId.isEmpty())
116
0
        aIdsToRemove.insert(aParCxn.msSibTransId);
117
118
0
    for (const Point& rPoint : maPoints)
119
0
        if (aIdsToRemove.count(rPoint.msPresentationAssociationId))
120
0
            aIdsToRemove.insert(rPoint.msModelId);
121
122
    // insert also transition nodes
123
0
    for (const auto& aCxn : maConnections)
124
0
        if (aIdsToRemove.count(aCxn.msSourceId) || aIdsToRemove.count(aCxn.msDestId))
125
0
            if (!aCxn.msPresId.isEmpty())
126
0
                aIdsToRemove.insert(aCxn.msPresId);
127
128
    // remove connections
129
0
    std::erase_if(maConnections,
130
0
                                       [&aIdsToRemove](const Connection& rCxn) {
131
0
                                           return aIdsToRemove.count(rCxn.msSourceId) || aIdsToRemove.count(rCxn.msDestId);
132
0
                                       });
133
134
    // remove data and presentation nodes
135
0
    std::erase_if(maPoints,
136
0
                                  [&aIdsToRemove](const Point& rPoint) {
137
0
                                      return aIdsToRemove.count(rPoint.msModelId);
138
0
                                  });
139
140
    // TODO: fix source/dest order
141
0
    return true;
142
0
}
143
144
DiagramDataState::DiagramDataState(Connections aConnections, Points aPoints)
145
0
: maConnections(std::move(aConnections))
146
0
, maPoints(std::move(aPoints))
147
0
{
148
0
}
149
150
DiagramDataStatePtr DiagramData::extractDiagramDataState() const
151
0
{
152
    // Just copy all Connections && Points. The shared_ptr data in
153
    // Point-entries is no problem, it just continues exiting shared
154
0
    return std::make_shared< DiagramDataState >(maConnections, maPoints);
155
0
}
156
157
void DiagramData::applyDiagramDataState(const DiagramDataStatePtr& rState)
158
0
{
159
0
    if(rState)
160
0
    {
161
0
        maConnections = rState->getConnections();
162
0
        maPoints = rState->getPoints();
163
164
        // Reset temporary buffered ModelData association lists & rebuild them
165
        // and the Diagram DataModel. Do that here *immediately* to prevent
166
        // re-usage of potentially invalid Connection/Point objects
167
0
        buildDiagramDataModel(true);
168
0
    }
169
0
}
170
171
void DiagramData::getChildrenString(
172
    OUStringBuffer& rBuf,
173
    const svx::diagram::Point* pPoint,
174
    sal_Int32 nLevel) const
175
0
{
176
0
    if (!pPoint)
177
0
        return;
178
179
0
    if (nLevel > 0)
180
0
    {
181
0
        for (sal_Int32 i = 0; i < nLevel-1; i++)
182
0
            rBuf.append('\t');
183
0
        rBuf.append('+');
184
0
        rBuf.append(' ');
185
0
        rBuf.append(pPoint->msTextBody->msText);
186
0
        rBuf.append('\n');
187
0
    }
188
189
0
    std::vector< const svx::diagram::Point* > aChildren;
190
0
    for (const auto& rCxn : maConnections)
191
0
        if (rCxn.mnXMLType == TypeConstant::XML_parOf && rCxn.msSourceId == pPoint->msModelId)
192
0
        {
193
0
            if (rCxn.mnSourceOrder >= static_cast<sal_Int32>(aChildren.size()))
194
0
                aChildren.resize(rCxn.mnSourceOrder + 1);
195
0
            const auto pChild = maPointNameMap.find(rCxn.msDestId);
196
0
            if (pChild != maPointNameMap.end())
197
0
                aChildren[rCxn.mnSourceOrder] = pChild->second;
198
0
        }
199
200
0
    for (auto pChild : aChildren)
201
0
        getChildrenString(rBuf, pChild, nLevel + 1);
202
0
}
203
204
std::vector<std::pair<OUString, OUString>> DiagramData::getChildren(const OUString& rParentId) const
205
0
{
206
0
    const OUString sModelId = rParentId.isEmpty() ? getRootPoint()->msModelId : rParentId;
207
0
    std::vector<std::pair<OUString, OUString>> aChildren;
208
0
    for (const auto& rCxn : maConnections)
209
0
        if (rCxn.mnXMLType == TypeConstant::XML_parOf && rCxn.msSourceId == sModelId)
210
0
        {
211
0
            if (rCxn.mnSourceOrder >= static_cast<sal_Int32>(aChildren.size()))
212
0
                aChildren.resize(rCxn.mnSourceOrder + 1);
213
0
            const auto pChild = maPointNameMap.find(rCxn.msDestId);
214
0
            if (pChild != maPointNameMap.end())
215
0
            {
216
0
                aChildren[rCxn.mnSourceOrder] = std::make_pair(
217
0
                    pChild->second->msModelId,
218
0
                    pChild->second->msTextBody->msText);
219
0
            }
220
0
        }
221
222
    // HACK: empty items shouldn't appear there
223
0
    std::erase_if(aChildren, [](const std::pair<OUString, OUString>& aItem) { return aItem.first.isEmpty(); });
224
225
0
    return aChildren;
226
0
}
227
228
OUString DiagramData::addNode(const OUString& rText)
229
0
{
230
0
    const svx::diagram::Point& rDataRoot = *getRootPoint();
231
0
    OUString sPresRoot;
232
0
    for (const auto& aCxn : maConnections)
233
0
        if (aCxn.mnXMLType == TypeConstant::XML_presOf && aCxn.msSourceId == rDataRoot.msModelId)
234
0
            sPresRoot = aCxn.msDestId;
235
236
0
    if (sPresRoot.isEmpty())
237
0
        return OUString();
238
239
0
    OUString sNewNodeId = OStringToOUString(comphelper::xml::generateGUIDString(), RTL_TEXTENCODING_UTF8);
240
241
0
    svx::diagram::Point aDataPoint;
242
0
    aDataPoint.mnXMLType = TypeConstant::XML_node;
243
0
    aDataPoint.msModelId = sNewNodeId;
244
0
    aDataPoint.msTextBody->msText = rText;
245
246
0
    OUString sDataSibling;
247
0
    for (const auto& aCxn : maConnections)
248
0
        if (aCxn.mnXMLType == TypeConstant::XML_parOf && aCxn.msSourceId == rDataRoot.msModelId)
249
0
            sDataSibling = aCxn.msDestId;
250
251
0
    OUString sPresSibling;
252
0
    for (const auto& aCxn : maConnections)
253
0
        if (aCxn.mnXMLType == TypeConstant::XML_presOf && aCxn.msSourceId == sDataSibling)
254
0
            sPresSibling = aCxn.msDestId;
255
256
0
    svx::diagram::Point aPresPoint;
257
0
    aPresPoint.mnXMLType = TypeConstant::XML_pres;
258
0
    aPresPoint.msModelId = OStringToOUString(comphelper::xml::generateGUIDString(), RTL_TEXTENCODING_UTF8);
259
260
0
    aPresPoint.msPresentationAssociationId = aDataPoint.msModelId;
261
0
    if (!sPresSibling.isEmpty())
262
0
    {
263
        // no idea where to get these values from, so copy from previous sibling
264
0
        const svx::diagram::Point* pSiblingPoint = maPointNameMap[sPresSibling];
265
0
        aPresPoint.msPresentationLayoutName = pSiblingPoint->msPresentationLayoutName;
266
0
        aPresPoint.msPresentationLayoutStyleLabel = pSiblingPoint->msPresentationLayoutStyleLabel;
267
0
        aPresPoint.mnLayoutStyleIndex = pSiblingPoint->mnLayoutStyleIndex;
268
0
        aPresPoint.mnLayoutStyleCount = pSiblingPoint->mnLayoutStyleCount;
269
0
    }
270
271
0
    addConnection(svx::diagram::TypeConstant::XML_parOf, rDataRoot.msModelId, aDataPoint.msModelId);
272
0
    addConnection(svx::diagram::TypeConstant::XML_presParOf, sPresRoot, aPresPoint.msModelId);
273
0
    addConnection(svx::diagram::TypeConstant::XML_presOf, aDataPoint.msModelId, aPresPoint.msModelId);
274
275
    // adding at the end, so that references are not invalidated in between
276
0
    maPoints.push_back(std::move(aDataPoint));
277
0
    maPoints.push_back(std::move(aPresPoint));
278
279
0
    return sNewNodeId;
280
0
}
281
282
void DiagramData::addConnection(svx::diagram::TypeConstant nType, const OUString& sSourceId, const OUString& sDestId)
283
0
{
284
0
    sal_Int32 nMaxOrd = -1;
285
0
    for (const auto& aCxn : maConnections)
286
0
        if (aCxn.mnXMLType == nType && aCxn.msSourceId == sSourceId)
287
0
            nMaxOrd = std::max(nMaxOrd, aCxn.mnSourceOrder);
288
289
0
    svx::diagram::Connection& rCxn = maConnections.emplace_back();
290
0
    rCxn.mnXMLType = nType;
291
0
    rCxn.msSourceId = sSourceId;
292
0
    rCxn.msDestId = sDestId;
293
0
    rCxn.mnSourceOrder = nMaxOrd + 1;
294
0
}
295
296
// #define DEBUG_OOX_DIAGRAM
297
#ifdef DEBUG_OOX_DIAGRAM
298
OString normalizeDotName( const OUString& rStr )
299
{
300
    OUStringBuffer aBuf;
301
    aBuf.append('N');
302
303
    const sal_Int32 nLen(rStr.getLength());
304
    sal_Int32 nCurrIndex(0);
305
    while( nCurrIndex < nLen )
306
    {
307
        const sal_Int32 aChar=rStr.iterateCodePoints(&nCurrIndex);
308
        if( aChar != '-' && aChar != '{' && aChar != '}' )
309
            aBuf.append((sal_Unicode)aChar);
310
    }
311
312
    return OUStringToOString(aBuf.makeStringAndClear(),
313
                                  RTL_TEXTENCODING_UTF8);
314
}
315
#endif
316
317
static sal_Int32 calcDepth( std::u16string_view rNodeName,
318
                            const svx::diagram::Connections& rCnx )
319
1.17k
{
320
    // find length of longest path in 'isChild' graph, ending with rNodeName
321
1.17k
    for (auto const& elem : rCnx)
322
22.0k
    {
323
22.0k
        if( !elem.msParTransId.isEmpty() &&
324
4.30k
            !elem.msSibTransId.isEmpty() &&
325
4.30k
            !elem.msSourceId.isEmpty() &&
326
4.30k
            !elem.msDestId.isEmpty() &&
327
4.30k
            elem.mnXMLType == TypeConstant::XML_parOf &&
328
4.30k
            rNodeName == elem.msDestId )
329
449
        {
330
449
            return calcDepth(elem.msSourceId, rCnx) + 1;
331
449
        }
332
22.0k
    }
333
334
727
    return 0;
335
1.17k
}
336
337
void DiagramData::buildDiagramDataModel(bool /*bClearOoxShapes*/)
338
143
{
339
    // build name-object maps
340
143
    maPointNameMap.clear();
341
143
    maPointsPresNameMap.clear();
342
143
    maConnectionNameMap.clear();
343
143
    maPresOfNameMap.clear();
344
143
    msBackgroundShapeModelID.clear();
345
346
#ifdef DEBUG_OOX_DIAGRAM
347
    std::ofstream output("tree.dot");
348
349
    output << "digraph datatree {" << std::endl;
350
#endif
351
143
    svx::diagram::Points& rPoints = getPoints();
352
143
    for (auto & point : rPoints)
353
2.10k
    {
354
#ifdef DEBUG_OOX_DIAGRAM
355
        output << "\t"
356
               << normalizeDotName(point.msModelId).getStr()
357
               << "[";
358
359
        if( !point.msPresentationLayoutName.isEmpty() )
360
            output << "label=\""
361
                   << OUStringToOString(
362
                       point.msPresentationLayoutName,
363
                       RTL_TEXTENCODING_UTF8).getStr() << "\", ";
364
        else
365
            output << "label=\""
366
                   << OUStringToOString(
367
                       point.msModelId,
368
                       RTL_TEXTENCODING_UTF8).getStr() << "\", ";
369
370
        switch( point.mnXMLType )
371
        {
372
            case TypeConstant::XML_doc: output << "style=filled, color=red"; break;
373
            case TypeConstant::XML_asst: output << "style=filled, color=green"; break;
374
            default:
375
            case TypeConstant::XML_node: output << "style=filled, color=blue"; break;
376
            case TypeConstant::XML_pres: output << "style=filled, color=yellow"; break;
377
            case TypeConstant::XML_parTrans: output << "color=grey"; break;
378
            case TypeConstant::XML_sibTrans: output << " "; break;
379
        }
380
381
        output << "];" << std::endl;
382
#endif
383
384
        // does currpoint have any text set?
385
2.10k
        if(!point.msTextBody->msText.isEmpty())
386
0
        {
387
#ifdef DEBUG_OOX_DIAGRAM
388
            static sal_Int32 nCount=0;
389
            output << "\t"
390
                   << "textNode" << nCount
391
                   << " ["
392
                   << "label=\""
393
                   << OUStringToOString(
394
                       point.msTextBody->msText,
395
                       RTL_TEXTENCODING_UTF8).getStr()
396
                   << "\"" << "];" << std::endl;
397
            output << "\t"
398
                   << normalizeDotName(point.msModelId).getStr()
399
                   << " -> "
400
                   << "textNode" << nCount++
401
                   << ";" << std::endl;
402
#endif
403
0
        }
404
405
2.10k
        const bool bInserted1 = getPointNameMap().insert(
406
2.10k
            std::make_pair(point.msModelId,&point)).second;
407
408
2.10k
        SAL_WARN_IF(!bInserted1, "oox.drawingml", "DiagramData::build(): non-unique point model id");
409
410
2.10k
        if( !point.msPresentationLayoutName.isEmpty() )
411
952
        {
412
952
            DiagramData::PointsNameMap::value_type::second_type& rVec=
413
952
                getPointsPresNameMap()[point.msPresentationLayoutName];
414
952
            rVec.push_back(&point);
415
952
        }
416
2.10k
    }
417
418
143
    const svx::diagram::Connections& rConnections = getConnections();
419
143
    for (auto const& connection : rConnections)
420
1.93k
    {
421
#ifdef DEBUG_OOX_DIAGRAM
422
        if( !connection.msParTransId.isEmpty() ||
423
            !connection.msSibTransId.isEmpty() )
424
        {
425
            if( !connection.msSourceId.isEmpty() ||
426
                !connection.msDestId.isEmpty() )
427
            {
428
                output << "\t"
429
                       << normalizeDotName(connection.msSourceId).getStr()
430
                       << " -> "
431
                       << normalizeDotName(connection.msParTransId).getStr()
432
                       << " -> "
433
                       << normalizeDotName(connection.msSibTransId).getStr()
434
                       << " -> "
435
                       << normalizeDotName(connection.msDestId).getStr()
436
                       << " [style=dotted,"
437
                       << ((connection.mnXMLType == TypeConstant::XML_presOf) ? " color=red, " : ((connection.mnXMLType == TypeConstant::XML_presParOf) ? " color=green, " : " "))
438
                       << "label=\""
439
                       << OUStringToOString(connection.msModelId,
440
                                                 RTL_TEXTENCODING_UTF8 ).getStr()
441
                       << "\"];" << std::endl;
442
            }
443
            else
444
            {
445
                output << "\t"
446
                       << normalizeDotName(connection.msParTransId).getStr()
447
                       << " -> "
448
                       << normalizeDotName(connection.msSibTransId).getStr()
449
                       << " ["
450
                       << ((connection.mnXMLType == TypeConstant::XML_presOf) ? " color=red, " : ((connection.mnXMLType == TypeConstant::XML_presParOf) ? " color=green, " : " "))
451
                       << "label=\""
452
                       << OUStringToOString(connection.msModelId,
453
                                                 RTL_TEXTENCODING_UTF8 ).getStr()
454
                       << "\"];" << std::endl;
455
            }
456
        }
457
        else if( !connection.msSourceId.isEmpty() ||
458
                 !connection.msDestId.isEmpty() )
459
            output << "\t"
460
                   << normalizeDotName(connection.msSourceId).getStr()
461
                   << " -> "
462
                   << normalizeDotName(connection.msDestId).getStr()
463
                   << " [label=\""
464
                   << OUStringToOString(connection.msModelId,
465
                                             RTL_TEXTENCODING_UTF8 ).getStr()
466
                   << ((connection.mnXMLType == TypeConstant::XML_presOf) ? "\", color=red]" : ((connection.mnXMLType == TypeConstant::XML_presParOf) ? "\", color=green]" : "\"]"))
467
                   << ";" << std::endl;
468
#endif
469
470
1.93k
        const bool bInserted1 = maConnectionNameMap.insert(
471
1.93k
            std::make_pair(connection.msModelId,&connection)).second;
472
473
1.93k
        SAL_WARN_IF(!bInserted1, "oox.drawingml", "DiagramData::build(): non-unique connection model id");
474
475
1.93k
        if( connection.mnXMLType == TypeConstant::XML_presOf )
476
727
        {
477
727
            DiagramData::StringMap::value_type::second_type& rVec = getPresOfNameMap()[connection.msDestId];
478
727
            rVec[connection.mnDestOrder] = { connection.msSourceId, sal_Int32(0) };
479
727
        }
480
1.93k
    }
481
482
    // assign outline levels
483
143
    DiagramData::StringMap& rStringMap = getPresOfNameMap();
484
143
    for (auto & elemPresOf : rStringMap)
485
727
    {
486
727
        for (auto & elem : elemPresOf.second)
487
727
        {
488
727
            const sal_Int32 nDepth = calcDepth(elem.second.msSourceId, getConnections());
489
727
            elem.second.mnDepth = nDepth != 0 ? nDepth : -1;
490
727
        }
491
727
    }
492
#ifdef DEBUG_OOX_DIAGRAM
493
    output << "}" << std::endl;
494
#endif
495
143
}
496
497
bool DiagramData::TextInformationChange(const OUString& rDiagramDataModelID, Outliner& rOutl)
498
0
{
499
    // try to get the Point for the associated ID
500
0
    const auto pDataNode = maPointNameMap.find(rDiagramDataModelID);
501
0
    if (pDataNode == maPointNameMap.end())
502
0
        return false;
503
504
    // use PresentationAssociationId to get to the text containing Point
505
0
    const OUString& rPresentationAssociationId(pDataNode->second->msPresentationAssociationId);
506
0
    if (rPresentationAssociationId.isEmpty())
507
0
        return false;
508
509
    // try to get the text-associated Point
510
0
    const auto pTextNode = maPointNameMap.find(rPresentationAssociationId);
511
0
    if (pTextNode == maPointNameMap.end())
512
0
        return false;
513
514
    // check if it has a text node - it should
515
0
    if (!pTextNode->second->msTextBody)
516
0
        return false;
517
518
    // access TextBody
519
0
    TextBodyPtr pTextBody(pTextNode->second->msTextBody);
520
521
    // now for outliner/source: Do we have data at all?
522
0
    if(0 == rOutl.GetParagraphCount())
523
0
        return false;
524
525
    // if yes, use 1st paragraph (for now)
526
0
    const Paragraph* pPara(rOutl.GetParagraph(0));
527
0
    if (nullptr == pPara)
528
0
        return false;
529
530
    // extract 1st para as text
531
0
    const OUString aCurrentText(rOutl.GetText(pPara));
532
533
    // check if text differs at all
534
0
    if (aCurrentText == pTextBody->msText)
535
0
        return false;
536
537
    // do change and return true (change was done)
538
    // NOTE: for now only rough text change, attributes are missing and will need to be added
539
0
    pTextBody->msText = aCurrentText;
540
0
    return true;
541
0
}
542
543
}
544
545
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */