1 module hunt.markdown.internal.ListBlockParser;
2 
3 import hunt.markdown.internal.util.Parsing;
4 import hunt.markdown.node.Node;
5 import hunt.markdown.node.Block;
6 import hunt.markdown.node.ListBlock;
7 import hunt.markdown.parser.block.AbstractBlockParser;
8 import hunt.markdown.parser.block.BlockContinue;
9 import hunt.markdown.parser.block.ParserState;
10 import hunt.markdown.parser.block.BlockStart;
11 import hunt.markdown.parser.block.AbstractBlockParserFactory;
12 import hunt.markdown.parser.block.MatchedBlockParser;
13 import hunt.markdown.node.ListItem;
14 import hunt.markdown.node.OrderedList;
15 import hunt.markdown.node.BulletList;
16 import hunt.markdown.parser.block.BlockParser;
17 import hunt.markdown.internal.ListItemParser;
18 
19 import hunt.text.Common;
20 import hunt.Integer;
21 import hunt.Char;
22 
23 class ListBlockParser : AbstractBlockParser {
24 
25     private ListBlock block;
26 
27     private bool hadBlankLine;
28     private int linesAfterBlank;
29 
30     public this(ListBlock block) {
31         this.block = block;
32     }
33 
34     override public bool isContainer() {
35         return true;
36     }
37 
38     override public bool canContain(Block childBlock) {
39         if (cast(ListItem)childBlock !is null) {
40             // Another list item is added to this list block. If the previous line was blank, that means this list block
41             // is "loose" (not tight).
42             //
43             // spec: A list is loose if any of its constituent list items are separated by blank lines
44             if (hadBlankLine && linesAfterBlank == 1) {
45                 assert(block !is null);
46                 block.setTight(false);
47                 hadBlankLine = false;
48             }
49             return true;
50         } else {
51             return false;
52         }
53     }
54 
55     override public Block getBlock() {
56         return block;
57     }
58 
59     public BlockContinue tryContinue(ParserState state) {
60         if (state.isBlank()) {
61             hadBlankLine = true;
62             linesAfterBlank = 0;
63         } else if (hadBlankLine) {
64             linesAfterBlank++;
65         }
66         // List blocks themselves don't have any markers, only list items. So try to stay in the list.
67         // If there is a block start other than list item, canContain makes sure that this list is closed.
68         return BlockContinue.atIndex(state.getIndex());
69     }
70 
71     /**
72      * Parse a list marker and return data on the marker or null.
73      */
74     private static ListData parseList(string line, int markerIndex, int markerColumn,
75                                       bool inParagraph) {
76         ListMarkerData listMarker = parseListMarker(line, markerIndex);
77         if (listMarker is null) {
78             return null;
79         }
80         ListBlock listBlock = listMarker.listBlock;
81 
82         int indexAfterMarker = listMarker.indexAfterMarker;
83         int markerLength = indexAfterMarker - markerIndex;
84         // marker doesn't include tabs, so counting them as columns directly is ok
85         int columnAfterMarker = markerColumn + markerLength;
86         // the column within the line where the content starts
87         int contentColumn = columnAfterMarker;
88 
89         // See at which column the content starts if there is content
90         bool hasContent = false;
91         int length = cast(int)(line.length);
92         for (int i = indexAfterMarker; i < length; i++) {
93             char c = line[i];
94             if (c == '\t') {
95                 contentColumn += Parsing.columnsToNextTabStop(contentColumn);
96             } else if (c == ' ') {
97                 contentColumn++;
98             } else {
99                 hasContent = true;
100                 break;
101             }
102         }
103 
104         if (inParagraph) {
105             // If the list item is ordered, the start number must be 1 to interrupt a paragraph.
106             if (cast(OrderedList)listBlock !is null && (cast(OrderedList) listBlock).getStartNumber() != 1) {
107                 return null;
108             }
109             // Empty list item can not interrupt a paragraph.
110             if (!hasContent) {
111                 return null;
112             }
113         }
114 
115         if (!hasContent || (contentColumn - columnAfterMarker) > Parsing.CODE_BLOCK_INDENT) {
116             // If this line is blank or has a code block, default to 1 space after marker
117             contentColumn = columnAfterMarker + 1;
118         }
119 
120         return new ListData(listBlock, contentColumn);
121     }
122 
123     private static ListMarkerData parseListMarker(string line, int index) {
124         char c = line[index];
125         switch (c) {
126             // spec: A bullet list marker is a -, +, or * character.
127             case '-':
128             case '+':
129             case '*':
130                 if (isSpaceTabOrEnd(line, index + 1)) {
131                     BulletList bulletList = new BulletList();
132                     bulletList.setBulletMarker(c);
133                     return new ListMarkerData(bulletList, index + 1);
134                 } else {
135                     return null;
136                 }
137             default:
138                 return parseOrderedList(line, index);
139         }
140     }
141 
142     // spec: An ordered list marker is a sequence of 1–9 arabic digits (0-9), followed by either a `.` character or a
143     // `)` character.
144     private static ListMarkerData parseOrderedList(string line, int index) {
145         int digits = 0;
146         int length = cast(int)(line.length);
147         for (int i = index; i < length; i++) {
148             char c = line[i];
149             switch (c) {
150                 case '0':
151                 case '1':
152                 case '2':
153                 case '3':
154                 case '4':
155                 case '5':
156                 case '6':
157                 case '7':
158                 case '8':
159                 case '9':
160                     digits++;
161                     if (digits > 9) {
162                         return null;
163                     }
164                     break;
165                 case '.':
166                 case ')':
167                     if (digits >= 1 && isSpaceTabOrEnd(line, i + 1)) {
168                         string number = line.substring(index, i);
169                         OrderedList orderedList = new OrderedList();
170                         orderedList.setStartNumber(Integer.parseInt(number));
171                         orderedList.setDelimiter(c);
172                         return new ListMarkerData(orderedList, i + 1);
173                     } else {
174                         return null;
175                     }
176                 default:
177                     return null;
178             }
179         }
180         return null;
181     }
182 
183     private static bool isSpaceTabOrEnd(string line, int index) {
184         if (index < line.length) {
185             switch (line[index]) {
186                 case ' ':
187                 case '\t':
188                     return true;
189                 default:
190                     return false;
191             }
192         } else {
193             return true;
194         }
195     }
196 
197     /**
198      * Returns true if the two list items are of the same type,
199      * with the same delimiter and bullet character. This is used
200      * in agglomerating list items into lists.
201      */
202     private static bool listsMatch(ListBlock a, ListBlock b) {
203         if (cast(BulletList)a !is null && cast(BulletList)b !is null) {
204             return (cast(BulletList) a).getBulletMarker() == (cast(BulletList) b).getBulletMarker();
205         } else if (cast(OrderedList)a !is null && cast(OrderedList)b !is null) {
206             return (cast(OrderedList) a).getDelimiter() == (cast(OrderedList) b).getDelimiter();
207         }
208         return false;
209     }
210 
211     private static bool equals(Object a, Object b) {
212         return (a is null) ? (b is null) : (a is b);
213     }
214 
215     public static class Factory : AbstractBlockParserFactory {
216 
217         public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
218             BlockParser matched = matchedBlockParser.getMatchedBlockParser();
219 
220             if (state.getIndent() >= Parsing.CODE_BLOCK_INDENT && !(cast(ListBlockParser)matched !is null)) {
221                 return BlockStart.none();
222             }
223             int markerIndex = state.getNextNonSpaceIndex();
224             int markerColumn = state.getColumn() + state.getIndent();
225             bool inParagraph = matchedBlockParser.getParagraphContent() !is null;
226             ListData listData = parseList(state.getLine(), markerIndex, markerColumn, inParagraph);
227             if (listData is null) {
228                 return BlockStart.none();
229             }
230 
231             int newColumn = listData.contentColumn;
232             ListItemParser listItemParser = new ListItemParser(newColumn - state.getColumn());
233 
234             // prepend the list block if needed
235             if (!(cast(ListBlockParser)matched !is null) ||
236                     !(listsMatch(cast(ListBlock) (matched.getBlock()), listData.listBlock))) {
237 
238                 ListBlockParser listBlockParser = new ListBlockParser(listData.listBlock);
239                 // We start out with assuming a list is tight. If we find a blank line, we set it to loose later.
240                 listData.listBlock.setTight(true);
241 
242                 return BlockStart.of(listBlockParser, listItemParser).atColumn(newColumn);
243             } else {
244                 return BlockStart.of(listItemParser).atColumn(newColumn);
245             }
246         }
247     }
248 
249     private static class ListData {
250         ListBlock listBlock;
251         int contentColumn;
252 
253         this(ListBlock listBlock, int contentColumn) {
254             this.listBlock = listBlock;
255             this.contentColumn = contentColumn;
256         }
257     }
258 
259     private static class ListMarkerData {
260         ListBlock listBlock;
261         int indexAfterMarker;
262 
263         this(ListBlock listBlock, int indexAfterMarker) {
264             this.listBlock = listBlock;
265             this.indexAfterMarker = indexAfterMarker;
266         }
267     }
268 }