1 module hunt.markdown.internal.HeadingParser;
2 
3 import hunt.markdown.internal.util.Parsing;
4 import hunt.markdown.node.Block;
5 import hunt.markdown.node.Heading;
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.text.Common;
15 
16 class HeadingParser : AbstractBlockParser {
17 
18     private Heading block;
19     private string content;
20 
21     public this(int level, string content) {
22         block = new Heading();
23         block.setLevel(level);
24         this.content = content;
25     }
26 
27     override public Block getBlock() {
28         return block;
29     }
30 
31     public BlockContinue tryContinue(ParserState parserState) {
32         // In both ATX and Setext headings, once we have the heading markup, there's nothing more to parse.
33         return BlockContinue.none();
34     }
35 
36     override public void parseInlines(InlineParser inlineParser) {
37         inlineParser.parse(content, block);
38     }
39 
40     public static class Factory : AbstractBlockParserFactory {
41 
42         public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
43             if (state.getIndent() >= Parsing.CODE_BLOCK_INDENT) {
44                 return BlockStart.none();
45             }
46 
47             string line = state.getLine();
48             int nextNonSpace = state.getNextNonSpaceIndex();
49             HeadingParser atxHeading = getAtxHeading(line, nextNonSpace);
50             if (atxHeading !is null) {
51                 return BlockStart.of(atxHeading).atIndex(cast(int)line.length);
52             }
53 
54             int setextHeadingLevel = getSetextHeadingLevel(line, nextNonSpace);
55             if (setextHeadingLevel > 0) {
56                 string paragraph = matchedBlockParser.getParagraphContent();
57                 if (paragraph !is null) {
58                     string content = paragraph;
59                     return BlockStart.of(new HeadingParser(setextHeadingLevel, content))
60                             .atIndex(cast(int)line.length)
61                             .replaceActiveBlockParser();
62                 }
63             }
64 
65             return BlockStart.none();
66         }
67     }
68 
69     // spec: An ATX heading consists of a string of characters, parsed as inline content, between an opening sequence of
70     // 1–6 unescaped # characters and an optional closing sequence of any number of unescaped # characters. The opening
71     // sequence of # characters must be followed by a space or by the end of line. The optional closing sequence of #s
72     // must be preceded by a space and may be followed by spaces only.
73     private static HeadingParser getAtxHeading(string line, int index) {
74         int level = Parsing.skip('#', line, index, cast(int)line.length) - index;
75 
76         if (level == 0 || level > 6) {
77             return null;
78         }
79 
80         int start = index + level;
81         if (start >= line.length) {
82             // End of line after markers is an empty heading
83             return new HeadingParser(level, "");
84         }
85 
86         char next = line[start];
87         if (!(next == ' ' || next == '\t')) {
88             return null;
89         }
90 
91         int beforeSpace = Parsing.skipSpaceTabBackwards(line, cast(int)line.length - 1, start);
92         int beforeHash = Parsing.skipBackwards('#', line, beforeSpace, start);
93         int beforeTrailer = Parsing.skipSpaceTabBackwards(line, beforeHash, start);
94         if (beforeTrailer != beforeHash) {
95             return new HeadingParser(level, line.substring(start, beforeTrailer + 1));
96         } else {
97             return new HeadingParser(level, line.substring(start, beforeSpace + 1));
98         }
99     }
100 
101     // spec: A setext heading underline is a sequence of = characters or a sequence of - characters, with no more than
102     // 3 spaces indentation and any number of trailing spaces.
103     private static int getSetextHeadingLevel(string line, int index) {
104         switch (line[index]) {
105             case '=':
106                 if (isSetextHeadingRest(line, index + 1, '=')) {
107                     return 1;
108                 }
109                 else
110                 {
111                     return 0;
112                 }
113             case '-':
114                 if (isSetextHeadingRest(line, index + 1, '-')) {
115                     return 2;
116                 }
117                 else
118                 {
119                     return 0;
120                 }
121             default:
122                 break;
123         }
124 
125         return 0;
126     }
127 
128     private static bool isSetextHeadingRest(string line, int index, char marker) {
129         int afterMarker = Parsing.skip(marker, line, index, cast(int)line.length);
130         int afterSpace = Parsing.skipSpaceTab(line, afterMarker, cast(int)line.length);
131         return afterSpace >= line.length;
132     }
133 }