1 module hunt.markdown.ext.table.internal.TableBlockParser; 2 3 import hunt.markdown.ext.table; 4 import hunt.markdown.node.Block; 5 import hunt.markdown.node.Node; 6 import hunt.markdown.parser.InlineParser; 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.AbstractBlockParserFactory; 11 import hunt.markdown.parser.block.BlockStart; 12 import hunt.markdown.parser.block.MatchedBlockParser; 13 14 import hunt.collection.ArrayList; 15 import hunt.collection.List; 16 17 import std.string; 18 import std.regex; 19 20 import hunt.text; 21 22 class TableBlockParser : AbstractBlockParser { 23 24 private enum string COL = "\\s*:?-{1,}:?\\s*"; 25 private enum string TABLE_HEADER_SEPARATOR = "\\|" ~ COL ~ "\\|?\\s*" ~ "|" ~ 26 COL ~ "\\|\\s*" ~ "|" ~ 27 "\\|?" ~ "(?:" ~ COL ~ "\\|)+" ~ COL ~ "\\|?\\s*"; 28 29 private TableBlock block; 30 private List!(string) rowLines; 31 32 private bool nextIsSeparatorLine = true; 33 private string separatorLine = ""; 34 35 // static this() 36 // { 37 // TABLE_HEADER_SEPARATOR = regex( 38 // // For single column, require at least one pipe, otherwise it's ambiguous with setext headers 39 // "\\|" ~ COL ~ "\\|?\\s*" ~ "|" ~ 40 // COL ~ "\\|\\s*" ~ "|" ~ 41 // "\\|?" ~ "(?:" ~ COL ~ "\\|)+" ~ COL ~ "\\|?\\s*"); 42 // } 43 44 private this(string headerLine) { 45 block = new TableBlock(); 46 rowLines = new ArrayList!(string)(); 47 rowLines.add(headerLine); 48 } 49 50 override public Block getBlock() { 51 return block; 52 } 53 54 public BlockContinue tryContinue(ParserState state) { 55 import std.algorithm; 56 57 if (state.getLine().find("|").empty) { 58 return BlockContinue.none(); 59 } else { 60 return BlockContinue.atIndex(state.getIndex()); 61 } 62 } 63 64 override public void addLine(string line) { 65 if (nextIsSeparatorLine) { 66 nextIsSeparatorLine = false; 67 separatorLine = line; 68 } else { 69 rowLines.add(line); 70 } 71 } 72 73 override public void parseInlines(InlineParser inlineParser) { 74 Node section = new TableHead(); 75 block.appendChild(section); 76 77 List!(TableCell.Alignment) alignments = parseAlignment(separatorLine); 78 79 int headerColumns = -1; 80 bool header = true; 81 foreach (string rowLine ; rowLines) { 82 List!(string) cells = split(rowLine); 83 TableRow tableRow = new TableRow(); 84 85 if (headerColumns == -1) { 86 headerColumns = cells.size(); 87 } 88 89 // Body can not have more columns than head 90 for (int i = 0; i < headerColumns; i++) { 91 string cell = i < cells.size() ? cells.get(i) : ""; 92 TableCell.Alignment alignment = alignments.get(i); 93 TableCell tableCell = new TableCell(); 94 tableCell.setHeader(header); 95 tableCell.setAlignment(alignment); 96 inlineParser.parse(cell.strip(), tableCell); 97 tableRow.appendChild(tableCell); 98 } 99 100 section.appendChild(tableRow); 101 102 if (header) { 103 // Format allows only one row in head 104 header = false; 105 section = new TableBody(); 106 block.appendChild(section); 107 } 108 } 109 } 110 111 private static List!(TableCell.Alignment) parseAlignment(string separatorLine) { 112 List!(string) parts = split(separatorLine); 113 List!(TableCell.Alignment) alignments = new ArrayList!(TableCell.Alignment)(); 114 foreach (string part ; parts) { 115 string trimmed = part.strip(); 116 bool left = trimmed.startsWith(":"); 117 bool right = trimmed.endsWith(":"); 118 TableCell.Alignment alignment = getAlignment(left, right); 119 alignments.add(alignment); 120 } 121 return alignments; 122 } 123 124 private static List!(string) split(string input) { 125 string line = input.strip(); 126 if (line.startsWith("|")) { 127 line = line.substring(1); 128 } 129 List!(string) cells = new ArrayList!(string)(); 130 StringBuilder sb = new StringBuilder(); 131 bool escape = false; 132 for (int i = 0; i < line.length; i++) { 133 char c = line[i]; 134 if (escape) { 135 escape = false; 136 sb.append(c); 137 } else { 138 switch (c) { 139 case '\\': 140 escape = true; 141 // Removing the escaping '\' is handled by the inline parser later, so add it to cell 142 sb.append(c); 143 break; 144 case '|': 145 cells.add(sb.toString()); 146 sb.setLength(0); 147 break; 148 default: 149 sb.append(c); 150 } 151 } 152 } 153 if (sb.length > 0) { 154 cells.add(sb.toString()); 155 } 156 return cells; 157 } 158 159 private static TableCell.Alignment getAlignment(bool left, bool right) { 160 if (left && right) { 161 return TableCell.Alignment.CENTER; 162 } else if (right) { 163 return TableCell.Alignment.RIGHT; 164 } else if (left) { 165 return TableCell.Alignment.LEFT; 166 } else { 167 return TableCell.Alignment.NONE; 168 } 169 } 170 171 public static class Factory : AbstractBlockParserFactory { 172 173 public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { 174 string line = state.getLine(); 175 string paragraph = matchedBlockParser.getParagraphContent(); 176 if (paragraph != null && paragraph.contains("|") && !paragraph.contains("\n")) { 177 string separatorLine = line[state.getIndex()..line.length]; 178 if (match(separatorLine, regex(TABLE_HEADER_SEPARATOR))) { 179 List!(string) headParts = split(paragraph); 180 List!(string) separatorParts = split(separatorLine); 181 if (separatorParts.size() >= headParts.size()) { 182 return BlockStart.of(new TableBlockParser(paragraph)) 183 .atIndex(state.getIndex()) 184 .replaceActiveBlockParser(); 185 } 186 } 187 } 188 return BlockStart.none(); 189 } 190 } 191 192 }