blob: 4f1ca53d7634c50a251586a933f3308abf96eee4 [file] [log] [blame]
Shawn Pearce12c8fab2016-05-15 16:55:21 -07001// Copyright (C) 2016 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.gitiles.doc;
16
17import com.google.common.base.Splitter;
18import com.google.common.primitives.Ints;
19import com.google.gitiles.doc.MultiColumnBlock.Column;
Dave Borowitz3b744b12016-08-19 16:11:10 -040020import java.util.ArrayList;
21import java.util.List;
Shawn Pearce12c8fab2016-05-15 16:55:21 -070022import org.commonmark.Extension;
23import org.commonmark.node.Block;
24import org.commonmark.node.Heading;
25import org.commonmark.node.Node;
26import org.commonmark.parser.Parser;
27import org.commonmark.parser.Parser.ParserExtension;
28import org.commonmark.parser.block.AbstractBlockParser;
29import org.commonmark.parser.block.AbstractBlockParserFactory;
30import org.commonmark.parser.block.BlockContinue;
31import org.commonmark.parser.block.BlockStart;
32import org.commonmark.parser.block.MatchedBlockParser;
33import org.commonmark.parser.block.ParserState;
34
Shawn Pearce12c8fab2016-05-15 16:55:21 -070035/** CommonMark extension for multicolumn layouts. */
36public class MultiColumnExtension implements ParserExtension {
37 private static final String MARKER = "|||---|||";
38
39 public static Extension create() {
40 return new MultiColumnExtension();
41 }
42
43 private MultiColumnExtension() {}
44
45 @Override
46 public void extend(Parser.Builder builder) {
Shawn Pearceac80c9c2016-06-05 09:48:54 -070047 builder.customBlockParserFactory(new MultiColumnParserFactory());
Shawn Pearce12c8fab2016-05-15 16:55:21 -070048 }
49
50 private static class MultiColumnParser extends AbstractBlockParser {
51 private final MultiColumnBlock block = new MultiColumnBlock();
52 private final List<Column> cols;
53 private boolean done;
54
55 MultiColumnParser(String layout) {
56 List<String> specList = Splitter.on(',').trimResults().splitToList(layout);
57 cols = new ArrayList<>(specList.size());
58 for (String spec : specList) {
59 cols.add(parseColumn(spec));
60 }
61 }
62
63 private MultiColumnBlock.Column parseColumn(String spec) {
64 MultiColumnBlock.Column col = new MultiColumnBlock.Column();
65 if (spec.startsWith(":")) {
66 col.empty = true;
67 spec = spec.substring(1);
68 }
69
70 Integer width = Ints.tryParse(spec, 10);
71 if (width != null) {
72 col.span = width;
73 }
74 return col;
75 }
76
77 @Override
78 public Block getBlock() {
79 return block;
80 }
81
82 @Override
83 public BlockContinue tryContinue(ParserState state) {
84 if (done) {
85 return BlockContinue.none();
86 }
87 if (state.getIndent() == 0) {
88 int s = state.getNextNonSpaceIndex();
89 CharSequence line = state.getLine();
90 if (MARKER.contentEquals(line.subSequence(s, line.length()))) {
91 done = true;
92 return BlockContinue.atIndex(line.length());
93 }
94 }
95 return BlockContinue.atIndex(state.getIndex());
96 }
97
98 @Override
99 public void closeBlock() {
100 splitChildren();
101 rebalanceSpans();
102
103 for (MultiColumnBlock.Column c : cols) {
104 block.appendChild(c);
105 }
106 }
107
108 private void splitChildren() {
109 int colIdx = 0;
110 Column col = null;
111 Node next = null;
112
113 for (Node child = block.getFirstChild(); child != null; child = next) {
114 if (col == null || child instanceof Heading || child instanceof BlockNote) {
115 for (; ; ) {
116 if (colIdx == cols.size()) {
117 cols.add(new Column());
118 }
119 col = cols.get(colIdx++);
120 if (!col.empty) {
121 break;
122 }
123 }
124 }
125 next = child.getNext();
126 col.appendChild(child);
127 }
128 }
129
130 private void rebalanceSpans() {
131 int remaining = MultiColumnBlock.GRID_WIDTH;
132 for (int i = 0; i < cols.size(); i++) {
133 Column col = cols.get(i);
134 if (col.span <= 0 || col.span > MultiColumnBlock.GRID_WIDTH) {
135 col.span = remaining / (cols.size() - i);
136 }
137 remaining = Math.max(0, remaining - col.span);
138 }
139 }
140
141 @Override
142 public boolean isContainer() {
143 return true;
144 }
145
146 @Override
147 public boolean canContain(Block block) {
148 return !(block instanceof MultiColumnBlock);
149 }
150 }
151
Shawn Pearceac80c9c2016-06-05 09:48:54 -0700152 private static class MultiColumnParserFactory extends AbstractBlockParserFactory {
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700153 @Override
154 public BlockStart tryStart(ParserState state, MatchedBlockParser matched) {
155 if (state.getIndent() > 0) {
156 return BlockStart.none();
157 }
158
159 int s = state.getNextNonSpaceIndex();
160 CharSequence line = state.getLine();
161 CharSequence text = line.subSequence(s, line.length());
Shawn Pearceda94d932016-06-05 09:56:25 -0700162 if (text.length() < MARKER.length()
163 || !MARKER.contentEquals(text.subSequence(0, MARKER.length()))) {
164 return BlockStart.none();
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700165 }
Shawn Pearceda94d932016-06-05 09:56:25 -0700166
167 String layout = text.subSequence(MARKER.length(), text.length()).toString().trim();
168 return BlockStart.of(new MultiColumnParser(layout)).atIndex(text.length());
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700169 }
170 }
171}