Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(273)

Side by Side Diff: Source/core/rendering/MultiColumnRow.cpp

Issue 883293004: [New Multicolumn] Preparatory work for nested multicol support. (Closed) Base URL: https://chromium.googlesource.com/chromium/blink.git@master
Patch Set: no find copies, please Created 5 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "config.h"
6
7 #include "core/rendering/MultiColumnRow.h"
8
9 #include "core/rendering/RenderMultiColumnSet.h"
10
11 namespace blink {
12
13 MultiColumnRow::MultiColumnRow(RenderMultiColumnSet& columnSet)
14 : m_columnSet(columnSet)
15 {
16 }
17
18 LayoutSize MultiColumnRow::offsetFromColumnSet() const
19 {
20 LayoutSize offset(LayoutUnit(), logicalTop());
21 if (!m_columnSet.flowThread()->isHorizontalWritingMode())
22 return offset.transposedSize();
23 return offset;
24 }
25
26 bool MultiColumnRow::heightIsAuto() const
27 {
28 // Only the last row may have auto height, and thus be balanced. There are n o good reasons to
29 // balance the preceding rows, and that could potentially lead to an insane number of layout
30 // passes as well.
31 return !nextRow() && m_columnSet.heightIsAuto();
32 }
33
34 void MultiColumnRow::resetColumnHeight()
35 {
36 // Nuke previously stored minimum column height. Contents may have changed f or all we know.
37 m_minimumColumnHeight = 0;
38
39 m_maxColumnHeight = calculateMaxColumnHeight();
40
41 LayoutUnit oldColumnHeight = m_columnHeight;
42
43 if (heightIsAuto())
44 m_columnHeight = LayoutUnit();
45 else
46 setAndConstrainColumnHeight(heightAdjustedForRowOffset(m_columnSet.multi ColumnFlowThread()->columnHeightAvailable()));
47
48 if (m_columnHeight != oldColumnHeight)
49 m_columnSet.setChildNeedsLayout(MarkOnlyThis);
50
51 // Content runs are only needed in the initial layout pass, in order to find an initial column
52 // height, and should have been deleted afterwards. We're about to rebuild t he content runs, so
53 // the list needs to be empty.
54 ASSERT(m_contentRuns.isEmpty());
55 }
56
57 void MultiColumnRow::addContentRun(LayoutUnit endOffsetInFlowThread)
58 {
59 if (!m_contentRuns.isEmpty() && endOffsetInFlowThread <= m_contentRuns.last( ).breakOffset())
60 return;
61 // Append another item as long as we haven't exceeded used column count. Wha t ends up in the
62 // overflow area shouldn't affect column balancing.
63 if (m_contentRuns.size() < m_columnSet.usedColumnCount())
64 m_contentRuns.append(ContentRun(endOffsetInFlowThread));
65 }
66
67 void MultiColumnRow::recordSpaceShortage(LayoutUnit spaceShortage)
68 {
69 if (spaceShortage >= m_minSpaceShortage)
70 return;
71
72 // The space shortage is what we use as our stretch amount. We need a positi ve number here in
73 // order to get anywhere.
74 ASSERT(spaceShortage > 0);
75
76 m_minSpaceShortage = spaceShortage;
77 }
78
79 bool MultiColumnRow::recalculateColumnHeight(BalancedColumnHeightCalculation cal culationMode)
80 {
81 LayoutUnit oldColumnHeight = m_columnHeight;
82
83 m_maxColumnHeight = calculateMaxColumnHeight();
84
85 if (heightIsAuto()) {
86 if (calculationMode == GuessFromFlowThreadPortion) {
87 // Post-process the content runs and find out where the implicit bre aks will occur.
88 distributeImplicitBreaks();
89 }
90 LayoutUnit newColumnHeight = calculateColumnHeight(calculationMode);
91 setAndConstrainColumnHeight(newColumnHeight);
92 // After having calculated an initial column height, the multicol contai ner typically needs at
93 // least one more layout pass with a new column height, but if a height was specified, we only
94 // need to do this if we think that we need less space than specified. C onversely, if we
95 // determined that the columns need to be as tall as the specified heigh t of the container, we
96 // have already laid it out correctly, and there's no need for another p ass.
97 } else {
98 // The position of the column set may have changed, in which case height available for
99 // columns may have changed as well.
100 setAndConstrainColumnHeight(m_columnHeight);
101 }
102
103 // We can get rid of the content runs now, if we haven't already done so. Th ey are only needed
104 // to calculate the initial balanced column height. In fact, we have to get rid of them before
105 // the next layout pass, since each pass will rebuild this.
106 m_contentRuns.clear();
107
108 if (m_columnHeight == oldColumnHeight)
109 return false; // No change. We're done.
110
111 m_minSpaceShortage = RenderFlowThread::maxLogicalHeight();
112 return true; // Need another pass.
113 }
114
115 void MultiColumnRow::expandToEncompassFlowThreadOverflow()
116 {
117 ASSERT(!nextRow());
118 // Get the offset within the flow thread in its block progression direction. Then get the
119 // flow thread's remaining logical height including its overflow and expand our rect
120 // to encompass that remaining height and overflow. The idea is that we will generate
121 // additional columns and pages to hold that overflow, since people do write bad
122 // content like <body style="height:0px"> in multi-column layouts.
123 RenderMultiColumnFlowThread* flowThread = m_columnSet.multiColumnFlowThread( );
124 LayoutRect layoutRect = flowThread->layoutOverflowRect();
125 m_logicalBottomInFlowThread = flowThread->isHorizontalWritingMode() ? layout Rect.maxY() : layoutRect.maxX();
126 }
127
128 LayoutSize MultiColumnRow::flowThreadTranslationAtOffset(LayoutUnit offsetInFlow Thread) const
129 {
130 unsigned columnIndex = columnIndexAtOffset(offsetInFlowThread);
131 LayoutRect portionRect(flowThreadPortionRectAt(columnIndex));
132 m_columnSet.flipForWritingMode(portionRect);
133 LayoutRect columnRect(columnRectAt(columnIndex));
134 m_columnSet.flipForWritingMode(columnRect);
135 return columnRect.location() - portionRect.location();
136 }
137
138 LayoutUnit MultiColumnRow::columnLogicalTopForOffset(LayoutUnit offsetInFlowThre ad) const
139 {
140 unsigned columnIndex = columnIndexAtOffset(offsetInFlowThread, AssumeNewColu mns);
141 return m_logicalTopInFlowThread + columnIndex * m_columnHeight;
142 }
143
144 void MultiColumnRow::collectLayerFragments(LayerFragments& fragments, const Layo utRect& layerBoundingBox, const LayoutRect& dirtyRect)
145 {
146 // |layerBoundingBox| is in the flow thread coordinate space, relative to th e top/left edge of
147 // the flow thread, but note that it has been converted with respect to writ ing mode (so that
148 // it's visual/physical in that sense).
149 //
150 // |dirtyRect| is visual, relative to the multicol container.
151 //
152 // Then there's the output from this method - the stuff we put into the list of fragments. The
153 // fragment.paginationOffset point is the actual visual translation required to get from a
154 // location in the flow thread to a location in a given column. The fragment .paginationClip
155 // rectangle, on the other hand, is in flow thread coordinates.
156 //
157 // All other rectangles in this method are sized physically, and the inline direction coordinate
158 // is physical too, but the block direction coordinate is "logical top". Thi s is the same as
159 // e.g. RenderBox::frameRect(). These rectangles also pretend that there's o nly one long column,
160 // i.e. they are for the flow thread.
161
162 RenderMultiColumnFlowThread* flowThread = m_columnSet.multiColumnFlowThread( );
163 bool isHorizontalWritingMode = m_columnSet.isHorizontalWritingMode();
164
165 // Put the layer bounds into flow thread-local coordinates by flipping it fi rst. Since we're in
166 // a renderer, most rectangles are represented this way.
167 LayoutRect layerBoundsInFlowThread(layerBoundingBox);
168 flowThread->flipForWritingMode(layerBoundsInFlowThread);
169
170 // Now we can compare with the flow thread portions owned by each column. Fi rst let's
171 // see if the rect intersects our flow thread portion at all.
172 LayoutRect clippedRect(layerBoundsInFlowThread);
173 clippedRect.intersect(m_columnSet.RenderRegion::flowThreadPortionOverflowRec t()); // FIXME: clean up this mess.
174 if (clippedRect.isEmpty())
175 return;
176
177 // Now we know we intersect at least one column. Let's figure out the logica l top and logical
178 // bottom of the area we're checking.
179 LayoutUnit layerLogicalTop = isHorizontalWritingMode ? layerBoundsInFlowThre ad.y() : layerBoundsInFlowThread.x();
180 LayoutUnit layerLogicalBottom = (isHorizontalWritingMode ? layerBoundsInFlow Thread.maxY() : layerBoundsInFlowThread.maxX()) - 1;
181
182 // Figure out the start and end columns and only check within that range so that we don't walk the
183 // entire column row.
184 unsigned startColumn = columnIndexAtOffset(layerLogicalTop);
185 unsigned endColumn = columnIndexAtOffset(layerLogicalBottom);
186
187 LayoutUnit colLogicalWidth = m_columnSet.pageLogicalWidth();
188 LayoutUnit colGap = m_columnSet.columnGap();
189 unsigned colCount = actualColumnCount();
190
191 bool progressionIsInline = flowThread->progressionIsInline();
192 bool leftToRight = m_columnSet.style()->isLeftToRightDirection();
193
194 LayoutUnit initialBlockOffset = m_columnSet.logicalTop() + logicalTop() - fl owThread->logicalTop();
195
196 for (unsigned i = startColumn; i <= endColumn; i++) {
197 // Get the portion of the flow thread that corresponds to this column.
198 LayoutRect flowThreadPortion = flowThreadPortionRectAt(i);
199
200 // Now get the overflow rect that corresponds to the column.
201 LayoutRect flowThreadOverflowPortion = flowThreadPortionOverflowRect(flo wThreadPortion, i, colCount, colGap);
202
203 // In order to create a fragment we must intersect the portion painted b y this column.
204 LayoutRect clippedRect(layerBoundsInFlowThread);
205 clippedRect.intersect(flowThreadOverflowPortion);
206 if (clippedRect.isEmpty())
207 continue;
208
209 // We also need to intersect the dirty rect. We have to apply a translat ion and shift based off
210 // our column index.
211 LayoutPoint translationOffset;
212 LayoutUnit inlineOffset = progressionIsInline ? i * (colLogicalWidth + c olGap) : LayoutUnit();
213 if (!leftToRight)
214 inlineOffset = -inlineOffset;
215 translationOffset.setX(inlineOffset);
216 LayoutUnit blockOffset;
217 if (progressionIsInline) {
218 blockOffset = initialBlockOffset + (isHorizontalWritingMode ? -flowT hreadPortion.y() : -flowThreadPortion.x());
219 } else {
220 // Column gap can apply in the block direction for page fragmentaine rs.
221 // There is currently no spec which calls for column-gap to apply
222 // for page fragmentainers at all, but it's applied here for compati bility
223 // with the old multicolumn implementation.
224 blockOffset = i * colGap;
225 }
226 if (isFlippedBlocksWritingMode(m_columnSet.style()->writingMode()))
227 blockOffset = -blockOffset;
228 translationOffset.setY(blockOffset);
229 if (!isHorizontalWritingMode)
230 translationOffset = translationOffset.transposedPoint();
231
232 // Shift the dirty rect to be in flow thread coordinates with this trans lation applied.
233 LayoutRect translatedDirtyRect(dirtyRect);
234 translatedDirtyRect.moveBy(-translationOffset);
235
236 // See if we intersect the dirty rect.
237 clippedRect = layerBoundingBox;
238 clippedRect.intersect(translatedDirtyRect);
239 if (clippedRect.isEmpty())
240 continue;
241
242 // Something does need to paint in this column. Make a fragment now and supply the physical translation
243 // offset and the clip rect for the column with that offset applied.
244 LayerFragment fragment;
245 fragment.paginationOffset = translationOffset;
246
247 LayoutRect flippedFlowThreadOverflowPortion(flowThreadOverflowPortion);
248 // Flip it into more a physical (RenderLayer-style) rectangle.
249 flowThread->flipForWritingMode(flippedFlowThreadOverflowPortion);
250 fragment.paginationClip = flippedFlowThreadOverflowPortion;
251 fragments.append(fragment);
252 }
253 }
254
255 LayoutRect MultiColumnRow::calculateOverflow() const
256 {
257 unsigned columnCount = actualColumnCount();
258 if (!columnCount)
259 return LayoutRect();
260 return columnRectAt(columnCount - 1);
261 }
262
263 unsigned MultiColumnRow::actualColumnCount() const
264 {
265 // We must always return a value of 1 or greater. Column count = 0 is a mean ingless situation,
266 // and will confuse and cause problems in other parts of the code.
267 if (!m_columnHeight)
268 return 1;
269
270 // Our flow thread portion determines our column count. We have as many colu mns as needed to fit
271 // all the content.
272 LayoutUnit flowThreadPortionHeight = logicalHeightInFlowThread();
273 if (!flowThreadPortionHeight)
274 return 1;
275
276 unsigned count = ceil(flowThreadPortionHeight.toFloat() / m_columnHeight.toF loat());
277 ASSERT(count >= 1);
278 return count;
279 }
280
281 LayoutUnit MultiColumnRow::heightAdjustedForRowOffset(LayoutUnit height) const
282 {
283 // Adjust for the top offset within the content box of the multicol containe r (containing
284 // block), unless we're in the first set. We know that the top offset for th e first set will be
285 // zero, but if the multicol container has non-zero top border or padding, t he set's top offset
286 // (initially being 0 and relative to the border box) will be negative until it has been laid
287 // out. Had we used this bogus offset, we would calculate the wrong height, and risk performing
288 // a wasted layout iteration. Of course all other sets (if any) have this pr oblem in the first
289 // layout pass too, but there's really nothing we can do there until the flo w thread has been
290 // laid out anyway.
291 if (m_columnSet.previousSiblingMultiColumnSet()) {
292 RenderBlockFlow* multicolBlock = m_columnSet.multiColumnBlockFlow();
293 LayoutUnit contentLogicalTop = m_columnSet.logicalTop() - multicolBlock- >borderAndPaddingBefore();
294 height -= contentLogicalTop;
295 }
296 height -= logicalTop();
297 return max(height, LayoutUnit(1)); // Let's avoid zero height, as that would probably cause an infinite amount of columns to be created.
298 }
299
300 LayoutUnit MultiColumnRow::calculateMaxColumnHeight() const
301 {
302 RenderBlockFlow* multicolBlock = m_columnSet.multiColumnBlockFlow();
303 RenderStyle* multicolStyle = multicolBlock->style();
304 LayoutUnit availableHeight = m_columnSet.multiColumnFlowThread()->columnHeig htAvailable();
305 LayoutUnit maxColumnHeight = availableHeight ? availableHeight : RenderFlowT hread::maxLogicalHeight();
306 if (!multicolStyle->logicalMaxHeight().isMaxSizeNone()) {
307 LayoutUnit logicalMaxHeight = multicolBlock->computeContentLogicalHeight (multicolStyle->logicalMaxHeight(), -1);
308 if (logicalMaxHeight != -1 && maxColumnHeight > logicalMaxHeight)
309 maxColumnHeight = logicalMaxHeight;
310 }
311 return heightAdjustedForRowOffset(maxColumnHeight);
312 }
313
314 void MultiColumnRow::setAndConstrainColumnHeight(LayoutUnit newHeight)
315 {
316 m_columnHeight = newHeight;
317 if (m_columnHeight > m_maxColumnHeight)
318 m_columnHeight = m_maxColumnHeight;
319 // FIXME: the height may also be affected by the enclosing pagination contex t, if any.
320 }
321
322 unsigned MultiColumnRow::findRunWithTallestColumns() const
323 {
324 unsigned indexWithLargestHeight = 0;
325 LayoutUnit largestHeight;
326 LayoutUnit previousOffset = m_logicalTopInFlowThread;
327 size_t runCount = m_contentRuns.size();
328 ASSERT(runCount);
329 for (size_t i = 0; i < runCount; i++) {
330 const ContentRun& run = m_contentRuns[i];
331 LayoutUnit height = run.columnLogicalHeight(previousOffset);
332 if (largestHeight < height) {
333 largestHeight = height;
334 indexWithLargestHeight = i;
335 }
336 previousOffset = run.breakOffset();
337 }
338 return indexWithLargestHeight;
339 }
340
341 void MultiColumnRow::distributeImplicitBreaks()
342 {
343 #if ENABLE(ASSERT)
344 // There should be no implicit breaks assumed at this point.
345 for (unsigned i = 0; i < m_contentRuns.size(); i++)
346 ASSERT(!m_contentRuns[i].assumedImplicitBreaks());
347 #endif // ENABLE(ASSERT)
348
349 // Insert a final content run to encompass all content. This will include ov erflow if this is
350 // the last set.
351 addContentRun(m_logicalBottomInFlowThread);
352 unsigned columnCount = m_contentRuns.size();
353
354 // If there is room for more breaks (to reach the used value of column-count ), imagine that we
355 // insert implicit breaks at suitable locations. At any given time, the cont ent run with the
356 // currently tallest columns will get another implicit break "inserted", whi ch will increase its
357 // column count by one and shrink its columns' height. Repeat until we have the desired total
358 // number of breaks. The largest column height among the runs will then be t he initial column
359 // height for the balancer to use.
360 while (columnCount < m_columnSet.usedColumnCount()) {
361 unsigned index = findRunWithTallestColumns();
362 m_contentRuns[index].assumeAnotherImplicitBreak();
363 columnCount++;
364 }
365 }
366
367 LayoutUnit MultiColumnRow::calculateColumnHeight(BalancedColumnHeightCalculation calculationMode) const
368 {
369 if (calculationMode == GuessFromFlowThreadPortion) {
370 // Initial balancing. Start with the lowest imaginable column height. We use the tallest
371 // content run (after having "inserted" implicit breaks), and find its s tart offset (by
372 // looking at the previous run's end offset, or, if there's no previous run, the set's start
373 // offset in the flow thread).
374 unsigned index = findRunWithTallestColumns();
375 LayoutUnit startOffset = index > 0 ? m_contentRuns[index - 1].breakOffse t() : m_logicalTopInFlowThread;
376 return std::max<LayoutUnit>(m_contentRuns[index].columnLogicalHeight(sta rtOffset), m_minimumColumnHeight);
377 }
378
379 if (actualColumnCount() <= m_columnSet.usedColumnCount()) {
380 // With the current column height, the content fits without creating ove rflowing columns. We're done.
381 return m_columnHeight;
382 }
383
384 if (m_contentRuns.size() >= m_columnSet.usedColumnCount()) {
385 // Too many forced breaks to allow any implicit breaks. Initial balancin g should already
386 // have set a good height. There's nothing more we should do.
387 return m_columnHeight;
388 }
389
390 // If the initial guessed column height wasn't enough, stretch it now. Stret ch by the lowest
391 // amount of space shortage found during layout.
392
393 ASSERT(m_minSpaceShortage > 0); // We should never _shrink_ the height!
394 ASSERT(m_minSpaceShortage != RenderFlowThread::maxLogicalHeight()); // If th is happens, we probably have a bug.
395 if (m_minSpaceShortage == RenderFlowThread::maxLogicalHeight())
396 return m_columnHeight; // So bail out rather than looping infinitely.
397
398 return m_columnHeight + m_minSpaceShortage;
399 }
400
401 LayoutRect MultiColumnRow::columnRectAt(unsigned columnIndex) const
402 {
403 LayoutUnit columnLogicalWidth = m_columnSet.pageLogicalWidth();
404 LayoutUnit columnLogicalHeight = m_columnHeight;
405 LayoutUnit columnLogicalTop;
406 LayoutUnit columnLogicalLeft;
407 LayoutUnit columnGap = m_columnSet.columnGap();
408
409 if (m_columnSet.multiColumnFlowThread()->progressionIsInline()) {
410 if (m_columnSet.style()->isLeftToRightDirection())
411 columnLogicalLeft += columnIndex * (columnLogicalWidth + columnGap);
412 else
413 columnLogicalLeft += m_columnSet.contentLogicalWidth() - columnLogic alWidth - columnIndex * (columnLogicalWidth + columnGap);
414 } else {
415 columnLogicalTop += columnIndex * (columnLogicalHeight + columnGap);
416 }
417
418 LayoutRect columnRect(columnLogicalLeft, columnLogicalTop, columnLogicalWidt h, columnLogicalHeight);
419 if (!m_columnSet.isHorizontalWritingMode())
420 return columnRect.transposedRect();
421 return columnRect;
422 }
423
424 LayoutRect MultiColumnRow::flowThreadPortionRectAt(unsigned columnIndex) const
425 {
426 LayoutUnit logicalTop = m_logicalTopInFlowThread + columnIndex * m_columnHei ght;
427 if (m_columnSet.isHorizontalWritingMode())
428 return LayoutRect(LayoutUnit(), logicalTop, m_columnSet.pageLogicalWidth (), m_columnHeight);
429 return LayoutRect(logicalTop, LayoutUnit(), m_columnHeight, m_columnSet.page LogicalWidth());
430 }
431
432 LayoutRect MultiColumnRow::flowThreadPortionOverflowRect(const LayoutRect& porti onRect, unsigned columnIndex, unsigned columnCount, LayoutUnit columnGap) const
433 {
434 // This function determines the portion of the flow thread that paints for t he column. Along the inline axis, columns are
435 // unclipped at outside edges (i.e., the first and last column in the set), and they clip to half the column
436 // gap along interior edges.
437 //
438 // In the block direction, we will not clip overflow out of the top of the f irst column, or out of the bottom of
439 // the last column. This applies only to the true first column and last colu mn across all column sets.
440 //
441 // FIXME: Eventually we will know overflow on a per-column basis, but we can 't do this until we have a painting
442 // mode that understands not to paint contents from a previous column in the overflow area of a following column.
443 // This problem applies to regions and pages as well and is not unique to co lumns.
444 bool isFirstColumn = !columnIndex;
445 bool isLastColumn = columnIndex == columnCount - 1;
446 bool isLTR = m_columnSet.style()->isLeftToRightDirection();
447 bool isLeftmostColumn = isLTR ? isFirstColumn : isLastColumn;
448 bool isRightmostColumn = isLTR ? isLastColumn : isFirstColumn;
449
450 // Calculate the overflow rectangle, based on the flow thread's, clipped at column logical
451 // top/bottom unless it's the first/last column.
452 LayoutRect overflowRect = m_columnSet.overflowRectForFlowThreadPortion(porti onRect, isFirstColumn && m_columnSet.isFirstRegion(), isLastColumn && m_columnSe t.isLastRegion());
453
454 // Avoid overflowing into neighboring columns, by clipping in the middle of adjacent column
455 // gaps. Also make sure that we avoid rounding errors.
456 if (m_columnSet.isHorizontalWritingMode()) {
457 if (!isLeftmostColumn)
458 overflowRect.shiftXEdgeTo(portionRect.x() - columnGap / 2);
459 if (!isRightmostColumn)
460 overflowRect.shiftMaxXEdgeTo(portionRect.maxX() + columnGap - column Gap / 2);
461 } else {
462 if (!isLeftmostColumn)
463 overflowRect.shiftYEdgeTo(portionRect.y() - columnGap / 2);
464 if (!isRightmostColumn)
465 overflowRect.shiftMaxYEdgeTo(portionRect.maxY() + columnGap - column Gap / 2);
466 }
467 return overflowRect;
468 }
469
470 unsigned MultiColumnRow::columnIndexAtOffset(LayoutUnit offsetInFlowThread, Colu mnIndexCalculationMode mode) const
471 {
472 // Handle the offset being out of range.
473 if (offsetInFlowThread < m_logicalTopInFlowThread)
474 return 0;
475 // If we're laying out right now, we cannot constrain against some logical b ottom, since it
476 // isn't known yet. Otherwise, just return the last column if we're past the logical bottom.
477 if (mode == ClampToExistingColumns) {
478 if (offsetInFlowThread >= m_logicalBottomInFlowThread)
479 return actualColumnCount() - 1;
480 }
481
482 if (m_columnHeight)
483 return (offsetInFlowThread - m_logicalTopInFlowThread).toFloat() / m_col umnHeight.toFloat();
484 return 0;
485 }
486
487 } // namespace blink
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698