blob: 4f1ca53d7634c50a251586a933f3308abf96eee4 [file] [log] [blame]
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gitiles.doc;
import com.google.common.base.Splitter;
import com.google.common.primitives.Ints;
import com.google.gitiles.doc.MultiColumnBlock.Column;
import java.util.ArrayList;
import java.util.List;
import org.commonmark.Extension;
import org.commonmark.node.Block;
import org.commonmark.node.Heading;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.parser.Parser.ParserExtension;
import org.commonmark.parser.block.AbstractBlockParser;
import org.commonmark.parser.block.AbstractBlockParserFactory;
import org.commonmark.parser.block.BlockContinue;
import org.commonmark.parser.block.BlockStart;
import org.commonmark.parser.block.MatchedBlockParser;
import org.commonmark.parser.block.ParserState;
/** CommonMark extension for multicolumn layouts. */
public class MultiColumnExtension implements ParserExtension {
private static final String MARKER = "|||---|||";
public static Extension create() {
return new MultiColumnExtension();
}
private MultiColumnExtension() {}
@Override
public void extend(Parser.Builder builder) {
builder.customBlockParserFactory(new MultiColumnParserFactory());
}
private static class MultiColumnParser extends AbstractBlockParser {
private final MultiColumnBlock block = new MultiColumnBlock();
private final List<Column> cols;
private boolean done;
MultiColumnParser(String layout) {
List<String> specList = Splitter.on(',').trimResults().splitToList(layout);
cols = new ArrayList<>(specList.size());
for (String spec : specList) {
cols.add(parseColumn(spec));
}
}
private MultiColumnBlock.Column parseColumn(String spec) {
MultiColumnBlock.Column col = new MultiColumnBlock.Column();
if (spec.startsWith(":")) {
col.empty = true;
spec = spec.substring(1);
}
Integer width = Ints.tryParse(spec, 10);
if (width != null) {
col.span = width;
}
return col;
}
@Override
public Block getBlock() {
return block;
}
@Override
public BlockContinue tryContinue(ParserState state) {
if (done) {
return BlockContinue.none();
}
if (state.getIndent() == 0) {
int s = state.getNextNonSpaceIndex();
CharSequence line = state.getLine();
if (MARKER.contentEquals(line.subSequence(s, line.length()))) {
done = true;
return BlockContinue.atIndex(line.length());
}
}
return BlockContinue.atIndex(state.getIndex());
}
@Override
public void closeBlock() {
splitChildren();
rebalanceSpans();
for (MultiColumnBlock.Column c : cols) {
block.appendChild(c);
}
}
private void splitChildren() {
int colIdx = 0;
Column col = null;
Node next = null;
for (Node child = block.getFirstChild(); child != null; child = next) {
if (col == null || child instanceof Heading || child instanceof BlockNote) {
for (; ; ) {
if (colIdx == cols.size()) {
cols.add(new Column());
}
col = cols.get(colIdx++);
if (!col.empty) {
break;
}
}
}
next = child.getNext();
col.appendChild(child);
}
}
private void rebalanceSpans() {
int remaining = MultiColumnBlock.GRID_WIDTH;
for (int i = 0; i < cols.size(); i++) {
Column col = cols.get(i);
if (col.span <= 0 || col.span > MultiColumnBlock.GRID_WIDTH) {
col.span = remaining / (cols.size() - i);
}
remaining = Math.max(0, remaining - col.span);
}
}
@Override
public boolean isContainer() {
return true;
}
@Override
public boolean canContain(Block block) {
return !(block instanceof MultiColumnBlock);
}
}
private static class MultiColumnParserFactory extends AbstractBlockParserFactory {
@Override
public BlockStart tryStart(ParserState state, MatchedBlockParser matched) {
if (state.getIndent() > 0) {
return BlockStart.none();
}
int s = state.getNextNonSpaceIndex();
CharSequence line = state.getLine();
CharSequence text = line.subSequence(s, line.length());
if (text.length() < MARKER.length()
|| !MARKER.contentEquals(text.subSequence(0, MARKER.length()))) {
return BlockStart.none();
}
String layout = text.subSequence(MARKER.length(), text.length()).toString().trim();
return BlockStart.of(new MultiColumnParser(layout)).atIndex(text.length());
}
}
}