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 }