layout/svg/nsSVGContainerFrame.cpp

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

michael@0 1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 // Main header first:
michael@0 7 #include "nsSVGContainerFrame.h"
michael@0 8
michael@0 9 // Keep others in (case-insensitive) order:
michael@0 10 #include "nsCSSFrameConstructor.h"
michael@0 11 #include "nsSVGEffects.h"
michael@0 12 #include "nsSVGElement.h"
michael@0 13 #include "nsSVGUtils.h"
michael@0 14 #include "nsSVGAnimatedTransformList.h"
michael@0 15 #include "SVGTextFrame.h"
michael@0 16 #include "RestyleManager.h"
michael@0 17
michael@0 18 using namespace mozilla;
michael@0 19
michael@0 20 NS_QUERYFRAME_HEAD(nsSVGContainerFrame)
michael@0 21 NS_QUERYFRAME_ENTRY(nsSVGContainerFrame)
michael@0 22 NS_QUERYFRAME_TAIL_INHERITING(nsSVGContainerFrameBase)
michael@0 23
michael@0 24 NS_QUERYFRAME_HEAD(nsSVGDisplayContainerFrame)
michael@0 25 NS_QUERYFRAME_ENTRY(nsSVGDisplayContainerFrame)
michael@0 26 NS_QUERYFRAME_ENTRY(nsISVGChildFrame)
michael@0 27 NS_QUERYFRAME_TAIL_INHERITING(nsSVGContainerFrame)
michael@0 28
michael@0 29 nsIFrame*
michael@0 30 NS_NewSVGContainerFrame(nsIPresShell* aPresShell,
michael@0 31 nsStyleContext* aContext)
michael@0 32 {
michael@0 33 nsIFrame *frame = new (aPresShell) nsSVGContainerFrame(aContext);
michael@0 34 // If we were called directly, then the frame is for a <defs> or
michael@0 35 // an unknown element type. In both cases we prevent the content
michael@0 36 // from displaying directly.
michael@0 37 frame->AddStateBits(NS_FRAME_IS_NONDISPLAY);
michael@0 38 return frame;
michael@0 39 }
michael@0 40
michael@0 41 NS_IMPL_FRAMEARENA_HELPERS(nsSVGContainerFrame)
michael@0 42 NS_IMPL_FRAMEARENA_HELPERS(nsSVGDisplayContainerFrame)
michael@0 43
michael@0 44 nsresult
michael@0 45 nsSVGContainerFrame::AppendFrames(ChildListID aListID,
michael@0 46 nsFrameList& aFrameList)
michael@0 47 {
michael@0 48 return InsertFrames(aListID, mFrames.LastChild(), aFrameList);
michael@0 49 }
michael@0 50
michael@0 51 nsresult
michael@0 52 nsSVGContainerFrame::InsertFrames(ChildListID aListID,
michael@0 53 nsIFrame* aPrevFrame,
michael@0 54 nsFrameList& aFrameList)
michael@0 55 {
michael@0 56 NS_ASSERTION(aListID == kPrincipalList, "unexpected child list");
michael@0 57 NS_ASSERTION(!aPrevFrame || aPrevFrame->GetParent() == this,
michael@0 58 "inserting after sibling frame with different parent");
michael@0 59
michael@0 60 mFrames.InsertFrames(this, aPrevFrame, aFrameList);
michael@0 61
michael@0 62 return NS_OK;
michael@0 63 }
michael@0 64
michael@0 65 nsresult
michael@0 66 nsSVGContainerFrame::RemoveFrame(ChildListID aListID,
michael@0 67 nsIFrame* aOldFrame)
michael@0 68 {
michael@0 69 NS_ASSERTION(aListID == kPrincipalList, "unexpected child list");
michael@0 70
michael@0 71 mFrames.DestroyFrame(aOldFrame);
michael@0 72 return NS_OK;
michael@0 73 }
michael@0 74
michael@0 75 bool
michael@0 76 nsSVGContainerFrame::UpdateOverflow()
michael@0 77 {
michael@0 78 if (mState & NS_FRAME_IS_NONDISPLAY) {
michael@0 79 // We don't maintain overflow rects.
michael@0 80 // XXX It would have be better if the restyle request hadn't even happened.
michael@0 81 return false;
michael@0 82 }
michael@0 83 return nsSVGContainerFrameBase::UpdateOverflow();
michael@0 84 }
michael@0 85
michael@0 86 /**
michael@0 87 * Traverses a frame tree, marking any SVGTextFrame frames as dirty
michael@0 88 * and calling InvalidateRenderingObservers() on it.
michael@0 89 *
michael@0 90 * The reason that this helper exists is because SVGTextFrame is special.
michael@0 91 * None of the other SVG frames ever need to be reflowed when they have the
michael@0 92 * NS_FRAME_IS_NONDISPLAY bit set on them because their PaintSVG methods
michael@0 93 * (and those of any containers that they can validly be contained within) do
michael@0 94 * not make use of mRect or overflow rects. "em" lengths, etc., are resolved
michael@0 95 * as those elements are painted.
michael@0 96 *
michael@0 97 * SVGTextFrame is different because its anonymous block and inline frames
michael@0 98 * need to be reflowed in order to get the correct metrics when things like
michael@0 99 * inherited font-size of an ancestor changes, or a delayed webfont loads and
michael@0 100 * applies.
michael@0 101 *
michael@0 102 * We assume that any change that requires the anonymous kid of an
michael@0 103 * SVGTextFrame to reflow will result in an NS_FRAME_IS_DIRTY reflow. When
michael@0 104 * that reflow reaches an NS_FRAME_IS_NONDISPLAY frame it would normally
michael@0 105 * stop, but this helper looks for any SVGTextFrame descendants of such
michael@0 106 * frames and marks them NS_FRAME_IS_DIRTY so that the next time that they are
michael@0 107 * painted their anonymous kid will first get the necessary reflow.
michael@0 108 */
michael@0 109 /* static */ void
michael@0 110 nsSVGContainerFrame::ReflowSVGNonDisplayText(nsIFrame* aContainer)
michael@0 111 {
michael@0 112 NS_ASSERTION(aContainer->GetStateBits() & NS_FRAME_IS_DIRTY,
michael@0 113 "expected aContainer to be NS_FRAME_IS_DIRTY");
michael@0 114 NS_ASSERTION((aContainer->GetStateBits() & NS_FRAME_IS_NONDISPLAY) ||
michael@0 115 !aContainer->IsFrameOfType(nsIFrame::eSVG),
michael@0 116 "it is wasteful to call ReflowSVGNonDisplayText on a container "
michael@0 117 "frame that is not NS_FRAME_IS_NONDISPLAY");
michael@0 118 for (nsIFrame* kid = aContainer->GetFirstPrincipalChild(); kid;
michael@0 119 kid = kid->GetNextSibling()) {
michael@0 120 nsIAtom* type = kid->GetType();
michael@0 121 if (type == nsGkAtoms::svgTextFrame) {
michael@0 122 static_cast<SVGTextFrame*>(kid)->ReflowSVGNonDisplayText();
michael@0 123 } else {
michael@0 124 if (kid->IsFrameOfType(nsIFrame::eSVG | nsIFrame::eSVGContainer) ||
michael@0 125 type == nsGkAtoms::svgForeignObjectFrame ||
michael@0 126 !kid->IsFrameOfType(nsIFrame::eSVG)) {
michael@0 127 ReflowSVGNonDisplayText(kid);
michael@0 128 }
michael@0 129 }
michael@0 130 }
michael@0 131 }
michael@0 132
michael@0 133 void
michael@0 134 nsSVGDisplayContainerFrame::Init(nsIContent* aContent,
michael@0 135 nsIFrame* aParent,
michael@0 136 nsIFrame* aPrevInFlow)
michael@0 137 {
michael@0 138 if (!(GetStateBits() & NS_STATE_IS_OUTER_SVG)) {
michael@0 139 AddStateBits(aParent->GetStateBits() & NS_STATE_SVG_CLIPPATH_CHILD);
michael@0 140 }
michael@0 141 nsSVGContainerFrame::Init(aContent, aParent, aPrevInFlow);
michael@0 142 }
michael@0 143
michael@0 144 void
michael@0 145 nsSVGDisplayContainerFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
michael@0 146 const nsRect& aDirtyRect,
michael@0 147 const nsDisplayListSet& aLists)
michael@0 148 {
michael@0 149 // mContent could be a XUL element so check for an SVG element before casting
michael@0 150 if (mContent->IsSVG() &&
michael@0 151 !static_cast<const nsSVGElement*>(mContent)->HasValidDimensions()) {
michael@0 152 return;
michael@0 153 }
michael@0 154 return BuildDisplayListForNonBlockChildren(aBuilder, aDirtyRect, aLists);
michael@0 155 }
michael@0 156
michael@0 157 nsresult
michael@0 158 nsSVGDisplayContainerFrame::InsertFrames(ChildListID aListID,
michael@0 159 nsIFrame* aPrevFrame,
michael@0 160 nsFrameList& aFrameList)
michael@0 161 {
michael@0 162 // memorize first old frame after insertion point
michael@0 163 // XXXbz once again, this would work a lot better if the nsIFrame
michael@0 164 // methods returned framelist iterators....
michael@0 165 nsIFrame* nextFrame = aPrevFrame ?
michael@0 166 aPrevFrame->GetNextSibling() : GetChildList(aListID).FirstChild();
michael@0 167 nsIFrame* firstNewFrame = aFrameList.FirstChild();
michael@0 168
michael@0 169 // Insert the new frames
michael@0 170 nsSVGContainerFrame::InsertFrames(aListID, aPrevFrame, aFrameList);
michael@0 171
michael@0 172 // If we are not a non-display SVG frame and we do not have a bounds update
michael@0 173 // pending, then we need to schedule one for our new children:
michael@0 174 if (!(GetStateBits() &
michael@0 175 (NS_FRAME_IS_DIRTY | NS_FRAME_HAS_DIRTY_CHILDREN |
michael@0 176 NS_FRAME_IS_NONDISPLAY))) {
michael@0 177 for (nsIFrame* kid = firstNewFrame; kid != nextFrame;
michael@0 178 kid = kid->GetNextSibling()) {
michael@0 179 nsISVGChildFrame* SVGFrame = do_QueryFrame(kid);
michael@0 180 if (SVGFrame) {
michael@0 181 NS_ABORT_IF_FALSE(!(kid->GetStateBits() & NS_FRAME_IS_NONDISPLAY),
michael@0 182 "Check for this explicitly in the |if|, then");
michael@0 183 bool isFirstReflow = (kid->GetStateBits() & NS_FRAME_FIRST_REFLOW);
michael@0 184 // Remove bits so that ScheduleBoundsUpdate will work:
michael@0 185 kid->RemoveStateBits(NS_FRAME_FIRST_REFLOW | NS_FRAME_IS_DIRTY |
michael@0 186 NS_FRAME_HAS_DIRTY_CHILDREN);
michael@0 187 // No need to invalidate the new kid's old bounds, so we just use
michael@0 188 // nsSVGUtils::ScheduleBoundsUpdate.
michael@0 189 nsSVGUtils::ScheduleReflowSVG(kid);
michael@0 190 if (isFirstReflow) {
michael@0 191 // Add back the NS_FRAME_FIRST_REFLOW bit:
michael@0 192 kid->AddStateBits(NS_FRAME_FIRST_REFLOW);
michael@0 193 }
michael@0 194 }
michael@0 195 }
michael@0 196 }
michael@0 197
michael@0 198 return NS_OK;
michael@0 199 }
michael@0 200
michael@0 201 nsresult
michael@0 202 nsSVGDisplayContainerFrame::RemoveFrame(ChildListID aListID,
michael@0 203 nsIFrame* aOldFrame)
michael@0 204 {
michael@0 205 nsSVGEffects::InvalidateRenderingObservers(aOldFrame);
michael@0 206
michael@0 207 // nsSVGContainerFrame::RemoveFrame doesn't call down into
michael@0 208 // nsContainerFrame::RemoveFrame, so it doesn't call FrameNeedsReflow. We
michael@0 209 // need to schedule a repaint and schedule an update to our overflow rects.
michael@0 210 SchedulePaint();
michael@0 211 PresContext()->RestyleManager()->PostRestyleEvent(
michael@0 212 mContent->AsElement(), nsRestyleHint(0), nsChangeHint_UpdateOverflow);
michael@0 213
michael@0 214 nsresult rv = nsSVGContainerFrame::RemoveFrame(aListID, aOldFrame);
michael@0 215
michael@0 216 if (!(GetStateBits() & (NS_FRAME_IS_NONDISPLAY | NS_STATE_IS_OUTER_SVG))) {
michael@0 217 nsSVGUtils::NotifyAncestorsOfFilterRegionChange(this);
michael@0 218 }
michael@0 219
michael@0 220 return rv;
michael@0 221 }
michael@0 222
michael@0 223 bool
michael@0 224 nsSVGDisplayContainerFrame::IsSVGTransformed(gfx::Matrix *aOwnTransform,
michael@0 225 gfx::Matrix *aFromParentTransform) const
michael@0 226 {
michael@0 227 bool foundTransform = false;
michael@0 228
michael@0 229 // Check if our parent has children-only transforms:
michael@0 230 nsIFrame *parent = GetParent();
michael@0 231 if (parent &&
michael@0 232 parent->IsFrameOfType(nsIFrame::eSVG | nsIFrame::eSVGContainer)) {
michael@0 233 foundTransform = static_cast<nsSVGContainerFrame*>(parent)->
michael@0 234 HasChildrenOnlyTransform(aFromParentTransform);
michael@0 235 }
michael@0 236
michael@0 237 // mContent could be a XUL element so check for an SVG element before casting
michael@0 238 if (mContent->IsSVG()) {
michael@0 239 nsSVGElement *content = static_cast<nsSVGElement*>(mContent);
michael@0 240 nsSVGAnimatedTransformList* transformList =
michael@0 241 content->GetAnimatedTransformList();
michael@0 242 if ((transformList && transformList->HasTransform()) ||
michael@0 243 content->GetAnimateMotionTransform()) {
michael@0 244 if (aOwnTransform) {
michael@0 245 *aOwnTransform = gfx::ToMatrix(content->PrependLocalTransformsTo(gfxMatrix(),
michael@0 246 nsSVGElement::eUserSpaceToParent));
michael@0 247 }
michael@0 248 foundTransform = true;
michael@0 249 }
michael@0 250 }
michael@0 251 return foundTransform;
michael@0 252 }
michael@0 253
michael@0 254 //----------------------------------------------------------------------
michael@0 255 // nsISVGChildFrame methods
michael@0 256
michael@0 257 nsresult
michael@0 258 nsSVGDisplayContainerFrame::PaintSVG(nsRenderingContext* aContext,
michael@0 259 const nsIntRect *aDirtyRect,
michael@0 260 nsIFrame* aTransformRoot)
michael@0 261 {
michael@0 262 NS_ASSERTION(!NS_SVGDisplayListPaintingEnabled() ||
michael@0 263 (mState & NS_FRAME_IS_NONDISPLAY) ||
michael@0 264 PresContext()->IsGlyph(),
michael@0 265 "If display lists are enabled, only painting of non-display "
michael@0 266 "SVG should take this code path");
michael@0 267
michael@0 268 const nsStyleDisplay *display = StyleDisplay();
michael@0 269 if (display->mOpacity == 0.0)
michael@0 270 return NS_OK;
michael@0 271
michael@0 272 for (nsIFrame* kid = mFrames.FirstChild(); kid;
michael@0 273 kid = kid->GetNextSibling()) {
michael@0 274 nsSVGUtils::PaintFrameWithEffects(aContext, aDirtyRect, kid, aTransformRoot);
michael@0 275 }
michael@0 276
michael@0 277 return NS_OK;
michael@0 278 }
michael@0 279
michael@0 280 nsIFrame*
michael@0 281 nsSVGDisplayContainerFrame::GetFrameForPoint(const nsPoint &aPoint)
michael@0 282 {
michael@0 283 NS_ASSERTION(!NS_SVGDisplayListHitTestingEnabled() ||
michael@0 284 (mState & NS_FRAME_IS_NONDISPLAY),
michael@0 285 "If display lists are enabled, only hit-testing of a "
michael@0 286 "clipPath's contents should take this code path");
michael@0 287 return nsSVGUtils::HitTestChildren(this, aPoint);
michael@0 288 }
michael@0 289
michael@0 290 nsRect
michael@0 291 nsSVGDisplayContainerFrame::GetCoveredRegion()
michael@0 292 {
michael@0 293 return nsSVGUtils::GetCoveredRegion(mFrames);
michael@0 294 }
michael@0 295
michael@0 296 void
michael@0 297 nsSVGDisplayContainerFrame::ReflowSVG()
michael@0 298 {
michael@0 299 NS_ASSERTION(nsSVGUtils::OuterSVGIsCallingReflowSVG(this),
michael@0 300 "This call is probably a wasteful mistake");
michael@0 301
michael@0 302 NS_ABORT_IF_FALSE(!(GetStateBits() & NS_FRAME_IS_NONDISPLAY),
michael@0 303 "ReflowSVG mechanism not designed for this");
michael@0 304
michael@0 305 NS_ABORT_IF_FALSE(GetType() != nsGkAtoms::svgOuterSVGFrame,
michael@0 306 "Do not call on outer-<svg>");
michael@0 307
michael@0 308 if (!nsSVGUtils::NeedsReflowSVG(this)) {
michael@0 309 return;
michael@0 310 }
michael@0 311
michael@0 312 // If the NS_FRAME_FIRST_REFLOW bit has been removed from our parent frame,
michael@0 313 // then our outer-<svg> has previously had its initial reflow. In that case
michael@0 314 // we need to make sure that that bit has been removed from ourself _before_
michael@0 315 // recursing over our children to ensure that they know too. Otherwise, we
michael@0 316 // need to remove it _after_ recursing over our children so that they know
michael@0 317 // the initial reflow is currently underway.
michael@0 318
michael@0 319 bool isFirstReflow = (mState & NS_FRAME_FIRST_REFLOW);
michael@0 320
michael@0 321 bool outerSVGHasHadFirstReflow =
michael@0 322 (GetParent()->GetStateBits() & NS_FRAME_FIRST_REFLOW) == 0;
michael@0 323
michael@0 324 if (outerSVGHasHadFirstReflow) {
michael@0 325 mState &= ~NS_FRAME_FIRST_REFLOW; // tell our children
michael@0 326 }
michael@0 327
michael@0 328 nsOverflowAreas overflowRects;
michael@0 329
michael@0 330 for (nsIFrame* kid = mFrames.FirstChild(); kid;
michael@0 331 kid = kid->GetNextSibling()) {
michael@0 332 nsISVGChildFrame* SVGFrame = do_QueryFrame(kid);
michael@0 333 if (SVGFrame) {
michael@0 334 NS_ABORT_IF_FALSE(!(kid->GetStateBits() & NS_FRAME_IS_NONDISPLAY),
michael@0 335 "Check for this explicitly in the |if|, then");
michael@0 336 kid->AddStateBits(mState & NS_FRAME_IS_DIRTY);
michael@0 337 SVGFrame->ReflowSVG();
michael@0 338
michael@0 339 // We build up our child frame overflows here instead of using
michael@0 340 // nsLayoutUtils::UnionChildOverflow since SVG frame's all use the same
michael@0 341 // frame list, and we're iterating over that list now anyway.
michael@0 342 ConsiderChildOverflow(overflowRects, kid);
michael@0 343 } else {
michael@0 344 // Inside a non-display container frame, we might have some
michael@0 345 // SVGTextFrames. We need to cause those to get reflowed in
michael@0 346 // case they are the target of a rendering observer.
michael@0 347 NS_ASSERTION(kid->GetStateBits() & NS_FRAME_IS_NONDISPLAY,
michael@0 348 "expected kid to be a NS_FRAME_IS_NONDISPLAY frame");
michael@0 349 if (kid->GetStateBits() & NS_FRAME_IS_DIRTY) {
michael@0 350 nsSVGContainerFrame* container = do_QueryFrame(kid);
michael@0 351 if (container && container->GetContent()->IsSVG()) {
michael@0 352 ReflowSVGNonDisplayText(container);
michael@0 353 }
michael@0 354 }
michael@0 355 }
michael@0 356 }
michael@0 357
michael@0 358 // <svg> can create an SVG viewport with an offset due to its
michael@0 359 // x/y/width/height attributes, and <use> can introduce an offset with an
michael@0 360 // empty mRect (any width/height is copied to an anonymous <svg> child).
michael@0 361 // Other than that containers should not set mRect since all other offsets
michael@0 362 // come from transforms, which are accounted for by nsDisplayTransform.
michael@0 363 // Note that we rely on |overflow:visible| to allow display list items to be
michael@0 364 // created for our children.
michael@0 365 NS_ABORT_IF_FALSE(mContent->Tag() == nsGkAtoms::svg ||
michael@0 366 (mContent->Tag() == nsGkAtoms::use &&
michael@0 367 mRect.Size() == nsSize(0,0)) ||
michael@0 368 mRect.IsEqualEdges(nsRect()),
michael@0 369 "Only inner-<svg>/<use> is expected to have mRect set");
michael@0 370
michael@0 371 if (isFirstReflow) {
michael@0 372 // Make sure we have our filter property (if any) before calling
michael@0 373 // FinishAndStoreOverflow (subsequent filter changes are handled off
michael@0 374 // nsChangeHint_UpdateEffects):
michael@0 375 nsSVGEffects::UpdateEffects(this);
michael@0 376 }
michael@0 377
michael@0 378 FinishAndStoreOverflow(overflowRects, mRect.Size());
michael@0 379
michael@0 380 // Remove state bits after FinishAndStoreOverflow so that it doesn't
michael@0 381 // invalidate on first reflow:
michael@0 382 mState &= ~(NS_FRAME_FIRST_REFLOW | NS_FRAME_IS_DIRTY |
michael@0 383 NS_FRAME_HAS_DIRTY_CHILDREN);
michael@0 384 }
michael@0 385
michael@0 386 void
michael@0 387 nsSVGDisplayContainerFrame::NotifySVGChanged(uint32_t aFlags)
michael@0 388 {
michael@0 389 NS_ABORT_IF_FALSE(aFlags & (TRANSFORM_CHANGED | COORD_CONTEXT_CHANGED),
michael@0 390 "Invalidation logic may need adjusting");
michael@0 391
michael@0 392 nsSVGUtils::NotifyChildrenOfSVGChange(this, aFlags);
michael@0 393 }
michael@0 394
michael@0 395 SVGBBox
michael@0 396 nsSVGDisplayContainerFrame::GetBBoxContribution(
michael@0 397 const Matrix &aToBBoxUserspace,
michael@0 398 uint32_t aFlags)
michael@0 399 {
michael@0 400 SVGBBox bboxUnion;
michael@0 401
michael@0 402 nsIFrame* kid = mFrames.FirstChild();
michael@0 403 while (kid) {
michael@0 404 nsIContent *content = kid->GetContent();
michael@0 405 nsISVGChildFrame* svgKid = do_QueryFrame(kid);
michael@0 406 // content could be a XUL element so check for an SVG element before casting
michael@0 407 if (svgKid && (!content->IsSVG() ||
michael@0 408 static_cast<const nsSVGElement*>(content)->HasValidDimensions())) {
michael@0 409
michael@0 410 gfxMatrix transform = gfx::ThebesMatrix(aToBBoxUserspace);
michael@0 411 if (content->IsSVG()) {
michael@0 412 transform = static_cast<nsSVGElement*>(content)->
michael@0 413 PrependLocalTransformsTo(transform);
michael@0 414 }
michael@0 415 // We need to include zero width/height vertical/horizontal lines, so we have
michael@0 416 // to use UnionEdges.
michael@0 417 bboxUnion.UnionEdges(svgKid->GetBBoxContribution(gfx::ToMatrix(transform), aFlags));
michael@0 418 }
michael@0 419 kid = kid->GetNextSibling();
michael@0 420 }
michael@0 421
michael@0 422 return bboxUnion;
michael@0 423 }

mercurial