| // 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()); | |
| } | |
| } | |
| } |