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 <basegfx/polygon/b2dpolypolygontools.hxx>
21 : #include <svgio/svgreader/svgdocument.hxx>
22 : #include <svgio/svgreader/svgnode.hxx>
23 : #include <svgio/svgreader/svgstyleattributes.hxx>
24 : #include <drawinglayer/primitive2d/objectinfoprimitive2d.hxx>
25 : #include <tools/urlobj.hxx>
26 :
27 :
28 : namespace svgio
29 : {
30 : namespace svgreader
31 : {
32 : /// #i125258#
33 131737 : bool SvgNode::supportsParentStyle() const
34 : {
35 131737 : return true;
36 : }
37 :
38 393 : const SvgStyleAttributes* SvgNode::getSvgStyleAttributes() const
39 : {
40 393 : return 0;
41 : }
42 :
43 10067 : void SvgNode::fillCssStyleVectorUsingHierarchyAndSelectors(
44 : const OUString& rClassStr,
45 : const SvgNode& rCurrent,
46 : const OUString& aConcatenated)
47 : {
48 10067 : const SvgDocument& rDocument = getDocument();
49 :
50 10067 : if(rDocument.hasGlobalCssStyleAttributes())
51 : {
52 16 : const SvgNode* pParent = rCurrent.getParent();
53 :
54 : // check for ID (highest priority)
55 16 : if(rCurrent.getId())
56 : {
57 0 : const OUString& rId = *rCurrent.getId();
58 :
59 0 : if(rId.getLength())
60 : {
61 : const OUString aNewConcatenated(
62 0 : OUString::createFromAscii("#") +
63 0 : rId +
64 0 : aConcatenated);
65 :
66 0 : if(pParent)
67 : {
68 : // check for combined selectors at parent firstso that higher specificity will be in front
69 0 : fillCssStyleVectorUsingHierarchyAndSelectors(rClassStr, *pParent, aNewConcatenated);
70 : }
71 :
72 0 : const SvgStyleAttributes* pNew = rDocument.findGlobalCssStyleAttributes(aNewConcatenated);
73 :
74 0 : if(pNew)
75 : {
76 : // add CssStyle if found
77 0 : maCssStyleVector.push_back(pNew);
78 0 : }
79 : }
80 : }
81 :
82 : // check for 'class' references (a list of entries is allowed)
83 16 : if(rCurrent.getClass())
84 : {
85 0 : const OUString& rClassList = *rCurrent.getClass();
86 0 : const sal_Int32 nLen(rClassList.getLength());
87 :
88 0 : if(nLen)
89 : {
90 0 : std::vector< OUString > aParts;
91 0 : sal_Int32 nPos(0);
92 0 : OUStringBuffer aToken;
93 :
94 0 : while(nPos < nLen)
95 : {
96 0 : const sal_Int32 nInitPos(nPos);
97 0 : copyToLimiter(rClassList, sal_Unicode(' '), nPos, aToken, nLen);
98 0 : skip_char(rClassList, sal_Unicode(' '), nPos, nLen);
99 0 : const OUString aPart(aToken.makeStringAndClear().trim());
100 :
101 0 : if(aPart.getLength())
102 : {
103 0 : aParts.push_back(aPart);
104 : }
105 :
106 0 : if(nInitPos == nPos)
107 : {
108 : OSL_ENSURE(false, "Could not interpret on current position (!)");
109 0 : nPos++;
110 : }
111 0 : }
112 :
113 0 : for(sal_uInt32 a(0); a < aParts.size(); a++)
114 : {
115 : const OUString aNewConcatenated(
116 0 : OUString::createFromAscii(".") +
117 0 : aParts[a] +
118 0 : aConcatenated);
119 :
120 0 : if(pParent)
121 : {
122 : // check for combined selectors at parent firstso that higher specificity will be in front
123 0 : fillCssStyleVectorUsingHierarchyAndSelectors(rClassStr, *pParent, aNewConcatenated);
124 : }
125 :
126 0 : const SvgStyleAttributes* pNew = rDocument.findGlobalCssStyleAttributes(aNewConcatenated);
127 :
128 0 : if(pNew)
129 : {
130 : // add CssStyle if found
131 0 : maCssStyleVector.push_back(pNew);
132 : }
133 0 : }
134 : }
135 : }
136 :
137 : // check for class-dependent references to CssStyles
138 16 : if(!rClassStr.isEmpty())
139 : {
140 16 : OUString aNewConcatenated(aConcatenated);
141 :
142 16 : if(!rCurrent.getId() && !rCurrent.getClass() && 0 == aConcatenated.indexOf(rClassStr))
143 : {
144 : // no new CssStyle Selector and already starts with rClassStr, do not concatenate;
145 : // we pass an 'empty' node (in the sense of CssStyle Selector)
146 : }
147 : else
148 : {
149 8 : aNewConcatenated = rClassStr + aConcatenated;
150 : }
151 :
152 16 : if(pParent)
153 : {
154 : // check for combined selectors at parent firstso that higher specificity will be in front
155 8 : fillCssStyleVectorUsingHierarchyAndSelectors(rClassStr, *pParent, aNewConcatenated);
156 : }
157 :
158 16 : const SvgStyleAttributes* pNew = rDocument.findGlobalCssStyleAttributes(aNewConcatenated);
159 :
160 16 : if(pNew)
161 : {
162 : // add CssStyle if found
163 6 : maCssStyleVector.push_back(pNew);
164 16 : }
165 : }
166 : }
167 10067 : }
168 :
169 10059 : void SvgNode::fillCssStyleVector(const OUString& rClassStr)
170 : {
171 : OSL_ENSURE(!mbCssStyleVectorBuilt, "OOps, fillCssStyleVector called double ?!?");
172 10059 : mbCssStyleVectorBuilt = true;
173 :
174 : // #i125293# If we have CssStyles we need to buuild a linked list of SvgStyleAttributes
175 : // which represent this for the current object. There are various methods to
176 : // specify CssStyles which need to be taken into account in a given order:
177 : // - local CssStyle (independent from global CssStyles at SvgDocument)
178 : // - 'id' CssStyle
179 : // - 'class' CssStyle(s)
180 : // - type-dependent elements (e..g. 'rect' for all rect elements)
181 : // - local attributes (rOriginal)
182 : // - inherited attributes (up the hierarchy)
183 : // The first four will be collected in maCssStyleVector for the current element
184 : // (once, this will not change) and be linked in the needed order using the
185 : // get/setCssStyleParent at the SvgStyleAttributes which will be used preferred in
186 : // member evaluation over the existing parent hierarchy
187 :
188 : // check for local CssStyle with highest priority
189 10059 : if(mpLocalCssStyle)
190 : {
191 : // if we have one, use as first entry
192 3499 : maCssStyleVector.push_back(mpLocalCssStyle);
193 : }
194 :
195 : // check the hierarchy for concatenated patterns of Selectors
196 10059 : fillCssStyleVectorUsingHierarchyAndSelectors(rClassStr, *this, OUString());
197 :
198 : // #i125329# find Css selector '*', add as last element if found
199 10059 : const SvgStyleAttributes* pNew = getDocument().findGlobalCssStyleAttributes("*");
200 :
201 10059 : if(pNew)
202 : {
203 : // add CssStyle for selector '*' if found
204 0 : maCssStyleVector.push_back(pNew);
205 : }
206 10059 : }
207 :
208 127924 : const SvgStyleAttributes* SvgNode::checkForCssStyle(const OUString& rClassStr, const SvgStyleAttributes& rOriginal) const
209 : {
210 127924 : if(!mbCssStyleVectorBuilt)
211 : {
212 : // build needed CssStyleVector for local node
213 10059 : const_cast< SvgNode* >(this)->fillCssStyleVector(rClassStr);
214 : }
215 :
216 127924 : if(maCssStyleVector.empty())
217 : {
218 : // return given original if no CssStlyes found
219 116525 : return &rOriginal;
220 : }
221 : else
222 : {
223 : // #i125293# rOriginal will be the last element in the linked list; use no CssStyleParent
224 : // there (reset it) to ensure that the parent hierarchy will be used when it's base
225 : // is referenced. This new chaining inserts the CssStyles before the original style,
226 : // this makes the whole process much safer since the original style when used will
227 : // be not different to the situation without CssStyles; thus loops which may be caused
228 : // by trying to use the parent hierarchy of the owner of the style will be avoided
229 : // already in this mechanism. It's still good to keep the supportsParentStyle
230 : // from #i125258# in place, though.
231 : // This chain building using pointers will be done every time when checkForCssStyle
232 : // is used (not the search, only the chaining). This is needed since the CssStyles
233 : // themselves will be potentially used multiple times. It is not expensive since it's
234 : // only changing some pointers.
235 : // The alternative would be to create the style hierarchy for every element (or even
236 : // for the element containing the hierarchy) in a vector of pointers and to use that.
237 : // Resetting the CssStyleParent on rOriginal is probably not needeed
238 : // but simply safer to do.
239 11399 : const_cast< SvgStyleAttributes& >(rOriginal).setCssStyleParent(0);
240 :
241 : // loop over the existing CssStyles and link them. There is a first one, take
242 : // as current
243 11399 : SvgStyleAttributes* pCurrent = const_cast< SvgStyleAttributes* >(maCssStyleVector[0]);
244 :
245 11431 : for(sal_uInt32 a(1); a < maCssStyleVector.size(); a++)
246 : {
247 32 : SvgStyleAttributes* pNext = const_cast< SvgStyleAttributes* >(maCssStyleVector[a]);
248 :
249 32 : pCurrent->setCssStyleParent(pNext);
250 32 : pCurrent = pNext;
251 : }
252 :
253 : // pCurrent is the last used CssStyle, let it point to the original style
254 11399 : pCurrent->setCssStyleParent(&rOriginal);
255 :
256 : // return 1st CssStyle as style chain start element (only for the
257 : // local element, still no hierarchy used here)
258 11399 : return maCssStyleVector[0];
259 : }
260 : }
261 :
262 10783 : SvgNode::SvgNode(
263 : SVGToken aType,
264 : SvgDocument& rDocument,
265 : SvgNode* pParent)
266 : : maType(aType),
267 : mrDocument(rDocument),
268 : mpParent(pParent),
269 : mpAlternativeParent(0),
270 : maChildren(),
271 : mpId(0),
272 : mpClass(0),
273 : maXmlSpace(XmlSpace_notset),
274 : maDisplay(Display_inline),
275 : maCssStyleVector(),
276 : mpLocalCssStyle(0),
277 10783 : mbCssStyleVectorBuilt(false)
278 : {
279 : OSL_ENSURE(SVGTokenUnknown != maType, "SvgNode with unknown type created (!)");
280 :
281 10783 : if(pParent)
282 : {
283 10483 : pParent->maChildren.push_back(this);
284 : }
285 : else
286 : {
287 : #ifdef DBG_UTIL
288 : if(SVGTokenSvg != getType())
289 : {
290 : OSL_ENSURE(false, "No parent for this node (!)");
291 : }
292 : #endif
293 : }
294 10783 : }
295 :
296 21566 : SvgNode::~SvgNode()
297 : {
298 32049 : while(maChildren.size())
299 : {
300 10483 : delete maChildren[maChildren.size() - 1];
301 10483 : maChildren.pop_back();
302 : }
303 :
304 10783 : if(mpId)
305 : {
306 2308 : delete mpId;
307 : }
308 :
309 10783 : if(mpClass)
310 : {
311 0 : delete mpClass;
312 : }
313 :
314 10783 : if(mpLocalCssStyle)
315 : {
316 3572 : delete mpLocalCssStyle;
317 : }
318 10783 : }
319 :
320 3572 : void SvgNode::readLocalCssStyle(const OUString& aContent)
321 : {
322 3572 : if(!mpLocalCssStyle)
323 : {
324 : // create LocalCssStyle if needed but not yet added
325 3572 : mpLocalCssStyle = new SvgStyleAttributes(*this);
326 : }
327 : else
328 : {
329 : // 2nd fill would be an error
330 : OSL_ENSURE(false, "Svg node has two local CssStyles, this may lead to problems (!)");
331 : }
332 :
333 3572 : if(mpLocalCssStyle)
334 : {
335 : // parse and set values to it
336 3572 : mpLocalCssStyle->readCssStyle(aContent);
337 : }
338 : else
339 : {
340 : OSL_ENSURE(false, "Could not get/create a local CssStyle for a node (!)");
341 : }
342 3572 : }
343 :
344 10783 : void SvgNode::parseAttributes(const com::sun::star::uno::Reference< com::sun::star::xml::sax::XAttributeList >& xAttribs)
345 : {
346 : // no longer need to pre-sort moving 'style' entries to the back so that
347 : // values get overwritten - that was the previous, not complete solution for
348 : // handling the priorities between svg and Css properties
349 10783 : const sal_uInt32 nAttributes(xAttribs->getLength());
350 :
351 40169 : for(sal_uInt32 a(0); a < nAttributes; a++)
352 : {
353 29386 : const OUString aTokenName(xAttribs->getNameByIndex(a));
354 29386 : const SVGToken aSVGToken(StrToSVGToken(aTokenName, false));
355 :
356 29386 : parseAttribute(aTokenName, aSVGToken, xAttribs->getValueByIndex(a));
357 29386 : }
358 10783 : }
359 :
360 0 : Display getDisplayFromContent(const OUString& aContent)
361 : {
362 0 : if(!aContent.isEmpty())
363 : {
364 0 : if(aContent.startsWith("inline"))
365 : {
366 0 : return Display_inline;
367 : }
368 0 : else if(aContent.startsWith("none"))
369 : {
370 0 : return Display_none;
371 : }
372 0 : else if(aContent.startsWith("inherit"))
373 : {
374 0 : return Display_inherit;
375 : }
376 0 : else if(aContent.startsWith("block"))
377 : {
378 0 : return Display_block;
379 : }
380 0 : else if(aContent.startsWith("list-item"))
381 : {
382 0 : return Display_list_item;
383 : }
384 0 : else if(aContent.startsWith("run-in"))
385 : {
386 0 : return Display_run_in;
387 : }
388 0 : else if(aContent.startsWith("compact"))
389 : {
390 0 : return Display_compact;
391 : }
392 0 : else if(aContent.startsWith("marker"))
393 : {
394 0 : return Display_marker;
395 : }
396 0 : else if(aContent.startsWith("table"))
397 : {
398 0 : return Display_table;
399 : }
400 0 : else if(aContent.startsWith("inline-table"))
401 : {
402 0 : return Display_inline_table;
403 : }
404 0 : else if(aContent.startsWith("table-row-group"))
405 : {
406 0 : return Display_table_row_group;
407 : }
408 0 : else if(aContent.startsWith("table-header-group"))
409 : {
410 0 : return Display_table_header_group;
411 : }
412 0 : else if(aContent.startsWith("table-footer-group"))
413 : {
414 0 : return Display_table_footer_group;
415 : }
416 0 : else if(aContent.startsWith("table-row"))
417 : {
418 0 : return Display_table_row;
419 : }
420 0 : else if(aContent.startsWith("table-column-group"))
421 : {
422 0 : return Display_table_column_group;
423 : }
424 0 : else if(aContent.startsWith("table-column"))
425 : {
426 0 : return Display_table_column;
427 : }
428 0 : else if(aContent.startsWith("table-cell"))
429 : {
430 0 : return Display_table_cell;
431 : }
432 0 : else if(aContent.startsWith("table-caption"))
433 : {
434 0 : return Display_table_caption;
435 : }
436 : }
437 :
438 : // return the default
439 0 : return Display_inline;
440 : }
441 :
442 29386 : void SvgNode::parseAttribute(const OUString& /*rTokenName*/, SVGToken aSVGToken, const OUString& aContent)
443 : {
444 29386 : switch(aSVGToken)
445 : {
446 : case SVGTokenId:
447 : {
448 2308 : if(!aContent.isEmpty())
449 : {
450 2308 : setId(&aContent);
451 : }
452 2308 : break;
453 : }
454 : case SVGTokenClass:
455 : {
456 0 : if(!aContent.isEmpty())
457 : {
458 0 : setClass(&aContent);
459 : }
460 0 : break;
461 : }
462 : case SVGTokenXmlSpace:
463 : {
464 292 : if(!aContent.isEmpty())
465 : {
466 292 : if(aContent.startsWith("default"))
467 : {
468 0 : setXmlSpace(XmlSpace_default);
469 : }
470 292 : else if(aContent.startsWith("preserve"))
471 : {
472 292 : setXmlSpace(XmlSpace_preserve);
473 : }
474 : }
475 292 : break;
476 : }
477 : case SVGTokenDisplay:
478 : {
479 0 : if(!aContent.isEmpty())
480 : {
481 0 : setDisplay(getDisplayFromContent(aContent));
482 : }
483 0 : break;
484 : }
485 : default:
486 : {
487 26786 : break;
488 : }
489 : }
490 29386 : }
491 :
492 7344 : void SvgNode::decomposeSvgNode(drawinglayer::primitive2d::Primitive2DSequence& rTarget, bool bReferenced) const
493 : {
494 7344 : if(Display_none == getDisplay())
495 : {
496 0 : return;
497 : }
498 :
499 7344 : const SvgStyleAttributes* pStyles = getSvgStyleAttributes();
500 7344 : if(pStyles && (Visibility_hidden == pStyles->getVisibility() || Visibility_collapse == pStyles->getVisibility()))
501 : {
502 0 : return;
503 : }
504 :
505 7344 : if(!bReferenced)
506 : {
507 19293 : if(SVGTokenDefs == getType() ||
508 12862 : SVGTokenSymbol == getType() ||
509 12769 : SVGTokenClipPathNode == getType() ||
510 12487 : SVGTokenMask == getType() ||
511 18729 : SVGTokenMarker == getType() ||
512 6149 : SVGTokenPattern == getType())
513 : {
514 : // do not decompose defs or symbol nodes (these hold only style-like
515 : // objects which may be used by referencing them) except when doing
516 : // so controlled referenced
517 :
518 : // also do not decompose ClipPaths and Masks. These should be embedded
519 : // in a defs node (which gets not decomposed by itself), but you never
520 : // know
521 :
522 : // also not directly used are Markers and Patterns, only indirecty used
523 : // by reference
524 :
525 : // #i121656# also do not decompose nodes which have display="none" set
526 : // as property
527 282 : return;
528 : }
529 : }
530 :
531 7062 : const SvgNodeVector& rChildren = getChildren();
532 :
533 7062 : if(!rChildren.empty())
534 : {
535 3685 : const sal_uInt32 nCount(rChildren.size());
536 :
537 13955 : for(sal_uInt32 a(0); a < nCount; a++)
538 : {
539 10270 : SvgNode* pCandidate = rChildren[a];
540 :
541 10270 : if(pCandidate && Display_none != pCandidate->getDisplay())
542 : {
543 10270 : const SvgStyleAttributes* pChildStyles = pCandidate->getSvgStyleAttributes();
544 10270 : if(pChildStyles && Visibility_hidden != pChildStyles->getVisibility())
545 : {
546 9970 : drawinglayer::primitive2d::Primitive2DSequence aNewTarget;
547 :
548 9970 : pCandidate->decomposeSvgNode(aNewTarget, bReferenced);
549 :
550 9970 : if(aNewTarget.hasElements())
551 : {
552 5006 : drawinglayer::primitive2d::appendPrimitive2DSequenceToPrimitive2DSequence(rTarget, aNewTarget);
553 9970 : }
554 : }
555 : }
556 : else if(!pCandidate)
557 : {
558 : OSL_ENSURE(false, "Null-Pointer in child node list (!)");
559 : }
560 : }
561 :
562 3685 : if(rTarget.hasElements())
563 : {
564 2401 : if(pStyles)
565 : {
566 : // check if we have Title or Desc
567 2401 : const OUString& rTitle = pStyles->getTitle();
568 2401 : const OUString& rDesc = pStyles->getDesc();
569 :
570 2401 : if(!rTitle.isEmpty() || !rDesc.isEmpty())
571 : {
572 : // default object name is empty
573 0 : OUString aObjectName;
574 :
575 : // use path as object name when outmost element
576 0 : if(SVGTokenSvg == getType())
577 : {
578 0 : aObjectName = getDocument().getAbsolutePath();
579 :
580 0 : if(!aObjectName.isEmpty())
581 : {
582 0 : INetURLObject aURL(aObjectName);
583 :
584 0 : aObjectName = aURL.getName(
585 : INetURLObject::LAST_SEGMENT,
586 : true,
587 0 : INetURLObject::DECODE_WITH_CHARSET);
588 : }
589 : }
590 :
591 : // pack in ObjectInfoPrimitive2D group
592 : const drawinglayer::primitive2d::Primitive2DReference xRef(
593 : new drawinglayer::primitive2d::ObjectInfoPrimitive2D(
594 : rTarget,
595 : aObjectName,
596 : rTitle,
597 0 : rDesc));
598 :
599 0 : rTarget = drawinglayer::primitive2d::Primitive2DSequence(&xRef, 1);
600 : }
601 : }
602 : }
603 : }
604 : }
605 :
606 0 : const basegfx::B2DRange SvgNode::getCurrentViewPort() const
607 : {
608 0 : if(getParent())
609 : {
610 0 : return getParent()->getCurrentViewPort();
611 : }
612 : else
613 : {
614 0 : return basegfx::B2DRange(); // return empty B2DRange
615 : }
616 : }
617 :
618 0 : double SvgNode::getCurrentFontSizeInherited() const
619 : {
620 0 : if(getParent())
621 : {
622 0 : return getParent()->getCurrentFontSize();
623 : }
624 : else
625 : {
626 0 : return 0.0;
627 : }
628 : }
629 :
630 0 : double SvgNode::getCurrentFontSize() const
631 : {
632 0 : if(getSvgStyleAttributes())
633 0 : return getSvgStyleAttributes()->getFontSize().solve(*this, xcoordinate);
634 :
635 0 : return getCurrentFontSizeInherited();
636 : }
637 :
638 0 : double SvgNode::getCurrentXHeightInherited() const
639 : {
640 0 : if(getParent())
641 : {
642 0 : return getParent()->getCurrentXHeight();
643 : }
644 : else
645 : {
646 0 : return 0.0;
647 : }
648 : }
649 :
650 0 : double SvgNode::getCurrentXHeight() const
651 : {
652 0 : if(getSvgStyleAttributes())
653 : // for XHeight, use FontSize currently
654 0 : return getSvgStyleAttributes()->getFontSize().solve(*this, ycoordinate);
655 :
656 0 : return getCurrentXHeightInherited();
657 : }
658 :
659 2308 : void SvgNode::setId(const OUString* pfId)
660 : {
661 2308 : if(mpId)
662 : {
663 0 : mrDocument.removeSvgNodeFromMapper(*mpId);
664 0 : delete mpId;
665 0 : mpId = 0;
666 : }
667 :
668 2308 : if(pfId)
669 : {
670 2308 : mpId = new OUString(*pfId);
671 2308 : mrDocument.addSvgNodeToMapper(*mpId, *this);
672 : }
673 2308 : }
674 :
675 0 : void SvgNode::setClass(const OUString* pfClass)
676 : {
677 0 : if(mpClass)
678 : {
679 0 : mrDocument.removeSvgNodeFromMapper(*mpClass);
680 0 : delete mpClass;
681 0 : mpClass = 0;
682 : }
683 :
684 0 : if(pfClass)
685 : {
686 0 : mpClass = new OUString(*pfClass);
687 0 : mrDocument.addSvgNodeToMapper(*mpClass, *this);
688 : }
689 0 : }
690 :
691 0 : XmlSpace SvgNode::getXmlSpace() const
692 : {
693 0 : if(maXmlSpace != XmlSpace_notset)
694 : {
695 0 : return maXmlSpace;
696 : }
697 :
698 0 : if(getParent())
699 : {
700 0 : return getParent()->getXmlSpace();
701 : }
702 :
703 : // default is XmlSpace_default
704 0 : return XmlSpace_default;
705 : }
706 :
707 : } // end of namespace svgreader
708 : } // end of namespace svgio
709 :
710 :
711 : /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|