blob: aeb3d89de69f27c540cf1334a4ae9df15c077a88 [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;
20
21import org.commonmark.Extension;
22import org.commonmark.node.Block;
23import org.commonmark.node.Heading;
24import org.commonmark.node.Node;
25import org.commonmark.parser.Parser;
26import org.commonmark.parser.Parser.ParserExtension;
27import org.commonmark.parser.block.AbstractBlockParser;
28import org.commonmark.parser.block.AbstractBlockParserFactory;
29import org.commonmark.parser.block.BlockContinue;
30import org.commonmark.parser.block.BlockStart;
31import org.commonmark.parser.block.MatchedBlockParser;
32import org.commonmark.parser.block.ParserState;
33
34import java.util.ArrayList;
35import java.util.List;
36
37/** CommonMark extension for multicolumn layouts. */
38public class MultiColumnExtension implements ParserExtension {
39 private static final String MARKER = "|||---|||";
40
41 public static Extension create() {
42 return new MultiColumnExtension();
43 }
44
45 private MultiColumnExtension() {}
46
47 @Override
48 public void extend(Parser.Builder builder) {
Shawn Pearceac80c9c2016-06-05 09:48:54 -070049 builder.customBlockParserFactory(new MultiColumnParserFactory());
Shawn Pearce12c8fab2016-05-15 16:55:21 -070050 }
51
52 private static class MultiColumnParser extends AbstractBlockParser {
53 private final MultiColumnBlock block = new MultiColumnBlock();
54 private final List<Column> cols;
55 private boolean done;
56
57 MultiColumnParser(String layout) {
58 List<String> specList = Splitter.on(',').trimResults().splitToList(layout);
59 cols = new ArrayList<>(specList.size());
60 for (String spec : specList) {
61 cols.add(parseColumn(spec));
62 }
63 }
64
65 private MultiColumnBlock.Column parseColumn(String spec) {
66 MultiColumnBlock.Column col = new MultiColumnBlock.Column();
67 if (spec.startsWith(":")) {
68 col.empty = true;
69 spec = spec.substring(1);
70 }
71
72 Integer width = Ints.tryParse(spec, 10);
73 if (width != null) {
74 col.span = width;
75 }
76 return col;
77 }
78
79 @Override
80 public Block getBlock() {
81 return block;
82 }
83
84 @Override
85 public BlockContinue tryContinue(ParserState state) {
86 if (done) {
87 return BlockContinue.none();
88 }
89 if (state.getIndent() == 0) {
90 int s = state.getNextNonSpaceIndex();
91 CharSequence line = state.getLine();
92 if (MARKER.contentEquals(line.subSequence(s, line.length()))) {
93 done = true;
94 return BlockContinue.atIndex(line.length());
95 }
96 }
97 return BlockContinue.atIndex(state.getIndex());
98 }
99
100 @Override
101 public void closeBlock() {
102 splitChildren();
103 rebalanceSpans();
104
105 for (MultiColumnBlock.Column c : cols) {
106 block.appendChild(c);
107 }
108 }
109
110 private void splitChildren() {
111 int colIdx = 0;
112 Column col = null;
113 Node next = null;
114
115 for (Node child = block.getFirstChild(); child != null; child = next) {
116 if (col == null || child instanceof Heading || child instanceof BlockNote) {
117 for (; ; ) {
118 if (colIdx == cols.size()) {
119 cols.add(new Column());
120 }
121 col = cols.get(colIdx++);
122 if (!col.empty) {
123 break;
124 }
125 }
126 }
127 next = child.getNext();
128 col.appendChild(child);
129 }
130 }
131
132 private void rebalanceSpans() {
133 int remaining = MultiColumnBlock.GRID_WIDTH;
134 for (int i = 0; i < cols.size(); i++) {
135 Column col = cols.get(i);
136 if (col.span <= 0 || col.span > MultiColumnBlock.GRID_WIDTH) {
137 col.span = remaining / (cols.size() - i);
138 }
139 remaining = Math.max(0, remaining - col.span);
140 }
141 }
142
143 @Override
144 public boolean isContainer() {
145 return true;
146 }
147
148 @Override
149 public boolean canContain(Block block) {
150 return !(block instanceof MultiColumnBlock);
151 }
152 }
153
Shawn Pearceac80c9c2016-06-05 09:48:54 -0700154 private static class MultiColumnParserFactory extends AbstractBlockParserFactory {
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700155 @Override
156 public BlockStart tryStart(ParserState state, MatchedBlockParser matched) {
157 if (state.getIndent() > 0) {
158 return BlockStart.none();
159 }
160
161 int s = state.getNextNonSpaceIndex();
162 CharSequence line = state.getLine();
163 CharSequence text = line.subSequence(s, line.length());
Shawn Pearceda94d932016-06-05 09:56:25 -0700164 if (text.length() < MARKER.length()
165 || !MARKER.contentEquals(text.subSequence(0, MARKER.length()))) {
166 return BlockStart.none();
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700167 }
Shawn Pearceda94d932016-06-05 09:56:25 -0700168
169 String layout = text.subSequence(MARKER.length(), text.length()).toString().trim();
170 return BlockStart.of(new MultiColumnParser(layout)).atIndex(text.length());
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700171 }
172 }
173}