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 }