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 }