// Process one line at a time, modifying a block. // Returns 0 if successful. curptr is changed to point to // the currently open block. public static void IncorporateLine(LineInfo line, ref Block curptr) { var ln = line.Line; Block last_matched_container; // offset is the char position in the line var offset = 0; // column is the virtual position in the line that takes TAB expansion into account var column = 0; // the char position of the first non-space char int first_nonspace; // the virtual position of the first non-space chart, that includes TAB expansion int first_nonspace_column; int matched; int i; ListData data; bool all_matched = true; Block cur = curptr; var blank = false; char curChar; int indent; // container starts at the document root. var container = cur.Top; // for each containing block, try to parse the associated line start. // bail out on failure: container will point to the last matching block. while (container.LastChild != null && container.LastChild.IsOpen) { container = container.LastChild; FindFirstNonspace(ln, offset, column, out first_nonspace, out first_nonspace_column, out curChar); indent = first_nonspace_column - column; blank = curChar == '\n'; switch (container.Tag) { case BlockTag.BlockQuote: { if (indent <= 3 && curChar == '>') { AdvanceOffset(ln, indent + 1, true, ref offset, ref column); if (ln[offset] == ' ') { offset++; } } else { all_matched = false; } break; } case BlockTag.ListItem: { if (indent >= container.ListData.MarkerOffset + container.ListData.Padding) { AdvanceOffset(ln, container.ListData.MarkerOffset + container.ListData.Padding, true, ref offset, ref column); } else if (blank && container.FirstChild != null) { // if container->first_child is NULL, then the opening line // of the list item was blank after the list marker; in this // case, we are done with the list item. AdvanceOffset(ln, first_nonspace - offset, false, ref offset, ref column); } else { all_matched = false; } break; } case BlockTag.IndentedCode: { if (indent >= CODE_INDENT) { AdvanceOffset(ln, CODE_INDENT, true, ref offset, ref column); } else if (blank) { AdvanceOffset(ln, first_nonspace - offset, false, ref offset, ref column); } else { all_matched = false; } break; } case BlockTag.AtxHeading: case BlockTag.SetextHeading: { // a heading can never contain more than one line all_matched = false; if (blank) { container.IsLastLineBlank = true; } break; } case BlockTag.FencedCode: { // -1 means we've seen closer if (container.FencedCodeData.FenceLength == -1) { all_matched = false; if (blank) { container.IsLastLineBlank = true; } } else { // skip optional spaces of fence offset i = container.FencedCodeData.FenceOffset; while (i > 0 && ln[offset] == ' ') { offset++; column++; i--; } } break; } case BlockTag.HtmlBlock: { // all other block types can accept blanks if (blank && container.HtmlBlockType >= HtmlBlockType.InterruptingBlock) { container.IsLastLineBlank = true; all_matched = false; } break; } case BlockTag.Paragraph: { if (blank) { container.IsLastLineBlank = true; all_matched = false; } break; } } if (!all_matched) { container = container.Parent; // back up to last matching block break; } } last_matched_container = container; // check to see if we've hit 2nd blank line, break out of list: if (blank && container.IsLastLineBlank) { BreakOutOfLists(ref container, line); } var maybeLazy = cur.Tag == BlockTag.Paragraph; // unless last matched container is code block, try new container starts: while (container.Tag != BlockTag.FencedCode && container.Tag != BlockTag.IndentedCode && container.Tag != BlockTag.HtmlBlock) { FindFirstNonspace(ln, offset, column, out first_nonspace, out first_nonspace_column, out curChar); indent = first_nonspace_column - column; blank = curChar == '\n'; var indented = indent >= CODE_INDENT; if (!indented && curChar == '>') { AdvanceOffset(ln, first_nonspace + 1 - offset, false, ref offset, ref column); // optional following character if (ln[offset] == ' ') { offset++; column++; } container = CreateChildBlock(container, line, BlockTag.BlockQuote, first_nonspace); } else if (!indented && curChar == '#' && 0 != (matched = Scanner.scan_atx_heading_start(ln, first_nonspace, ln.Length, out i))) { AdvanceOffset(ln, first_nonspace + matched - offset, false, ref offset, ref column); container = CreateChildBlock(container, line, BlockTag.AtxHeading, first_nonspace); container.Heading = new HeadingData(i); } else if (!indented && (curChar == '`' || curChar == '~') && 0 != (matched = Scanner.scan_open_code_fence(ln, first_nonspace, ln.Length))) { container = CreateChildBlock(container, line, BlockTag.FencedCode, first_nonspace); container.FencedCodeData = new FencedCodeData(); container.FencedCodeData.FenceChar = curChar; container.FencedCodeData.FenceLength = matched; container.FencedCodeData.FenceOffset = first_nonspace - offset; AdvanceOffset(ln, first_nonspace + matched - offset, false, ref offset, ref column); } else if (!indented && curChar == '<' && (0 != (matched = (int)Scanner.scan_html_block_start(ln, first_nonspace, ln.Length)) || (container.Tag != BlockTag.Paragraph && 0 != (matched = (int)Scanner.scan_html_block_start_7(ln, first_nonspace, ln.Length))) )) { container = CreateChildBlock(container, line, BlockTag.HtmlBlock, first_nonspace); container.HtmlBlockType = (HtmlBlockType)matched; // note, we don't adjust offset because the tag is part of the text } else if (!indented && container.Tag == BlockTag.Paragraph && (curChar == '=' || curChar == '-') && 0 != (matched = Scanner.scan_setext_heading_line(ln, first_nonspace, ln.Length)) && ContainsSingleLine(container.StringContent)) { container.Tag = BlockTag.SetextHeading; container.Heading = new HeadingData(matched); AdvanceOffset(ln, ln.Length - 1 - offset, false, ref offset, ref column); } else if (!indented && !(container.Tag == BlockTag.Paragraph && !all_matched) && 0 != (Scanner.scan_thematic_break(ln, first_nonspace, ln.Length))) { // it's only now that we know the line is not part of a setext heading: container = CreateChildBlock(container, line, BlockTag.ThematicBreak, first_nonspace); Finalize(container, line); container = container.Parent; AdvanceOffset(ln, ln.Length - 1 - offset, false, ref offset, ref column); } else if ((!indented || container.Tag == BlockTag.List) && 0 != (matched = ParseListMarker(ln, first_nonspace, out data))) { // compute padding: AdvanceOffset(ln, first_nonspace + matched - offset, false, ref offset, ref column); i = 0; while (i <= 5 && ln[offset + i] == ' ') { i++; } // i = number of spaces after marker, up to 5 if (i >= 5 || i < 1 || ln[offset] == '\n') { data.Padding = matched + 1; if (i > 0) { column++; offset++; } } else { data.Padding = matched + i; AdvanceOffset(ln, i, true, ref offset, ref column); } // check container; if it's a list, see if this list item // can continue the list; otherwise, create a list container. data.MarkerOffset = indent; if (container.Tag != BlockTag.List || !ListsMatch(container.ListData, data)) { container = CreateChildBlock(container, line, BlockTag.List, first_nonspace); container.ListData = data; } // add the list item container = CreateChildBlock(container, line, BlockTag.ListItem, first_nonspace); container.ListData = data; } else if (indented && !maybeLazy && !blank) { AdvanceOffset(ln, CODE_INDENT, true, ref offset, ref column); container = CreateChildBlock(container, line, BlockTag.IndentedCode, offset); } else { break; } if (AcceptsLines(container.Tag)) { // if it's a line container, it can't contain other containers break; } maybeLazy = false; } // what remains at offset is a text line. add the text to the // appropriate container. FindFirstNonspace(ln, offset, column, out first_nonspace, out first_nonspace_column, out curChar); indent = first_nonspace_column - column; blank = curChar == '\n'; if (blank && container.LastChild != null) { container.LastChild.IsLastLineBlank = true; } // block quote lines are never blank as they start with > // and we don't count blanks in fenced code for purposes of tight/loose // lists or breaking out of lists. we also don't set last_line_blank // on an empty list item. container.IsLastLineBlank = (blank && container.Tag != BlockTag.BlockQuote && container.Tag != BlockTag.SetextHeading && container.Tag != BlockTag.FencedCode && !(container.Tag == BlockTag.ListItem && container.FirstChild == null && container.SourcePosition >= line.LineOffset)); Block cont = container; while (cont.Parent != null) { cont.Parent.IsLastLineBlank = false; cont = cont.Parent; } if (cur != last_matched_container && container == last_matched_container && !blank && cur.Tag == BlockTag.Paragraph && cur.StringContent.Length > 0) { AddLine(cur, line, ln, offset); } else { // not a lazy continuation // finalize any blocks that were not matched and set cur to container: while (cur != last_matched_container) { Finalize(cur, line); cur = cur.Parent; if (cur == null) { throw new CommonMarkException("Cannot finalize container block. Last matched container tag = " + last_matched_container.Tag); } } if (container.Tag == BlockTag.IndentedCode) { AddLine(container, line, ln, offset); } else if (container.Tag == BlockTag.FencedCode) { if ((indent <= 3 && curChar == container.FencedCodeData.FenceChar) && (0 != Scanner.scan_close_code_fence(ln, first_nonspace, container.FencedCodeData.FenceLength, ln.Length))) { // if closing fence, set fence length to -1. it will be closed when the next line is processed. container.FencedCodeData.FenceLength = -1; } else { AddLine(container, line, ln, offset); } } else if (container.Tag == BlockTag.HtmlBlock) { AddLine(container, line, ln, offset); if (Scanner.scan_html_block_end(container.HtmlBlockType, ln, first_nonspace, ln.Length)) { Finalize(container, line); container = container.Parent; } } else if (blank) { // ??? do nothing } else if (container.Tag == BlockTag.AtxHeading) { int p = ln.Length - 1; // trim trailing spaces while (p >= 0 && (ln[p] == ' ' || ln[p] == '\n')) { p--; } // if string ends in #s, remove these: while (p >= 0 && ln[p] == '#') { p--; } // there must be a space before the last hashtag if (p < 0 || ln[p] != ' ') { p = ln.Length - 1; } AddLine(container, line, ln, first_nonspace, p - first_nonspace + 1); Finalize(container, line); container = container.Parent; } else if (AcceptsLines(container.Tag)) { AddLine(container, line, ln, first_nonspace); } else if (container.Tag != BlockTag.ThematicBreak && container.Tag != BlockTag.SetextHeading) { // create paragraph container for line container = CreateChildBlock(container, line, BlockTag.Paragraph, first_nonspace); AddLine(container, line, ln, first_nonspace); } else { Utilities.Warning("Line {0} with container type {1} did not match any condition:\n\"{2}\"", line.LineNumber, container.Tag, ln); } curptr = container; } }
/// <summary> /// Attempts to parse a list item marker (bullet or enumerated). /// On success, returns length of the marker, and populates /// data with the details. On failure, returns 0. /// </summary> /// <remarks>Original: int parse_list_marker(string ln, int pos, ref ListData dataptr)</remarks> private static int ParseListMarker(string ln, int pos, out ListData data) { char c; int startpos; data = null; var len = ln.Length; startpos = pos; c = ln[pos]; if (c == '+' || c == '•' || ((c == '*' || c == '-') && 0 == Scanner.scan_thematic_break(ln, pos, len))) { pos++; if (pos == len || (ln[pos] != ' ' && ln[pos] != '\n')) { return(0); } data = new ListData(); data.BulletChar = c; data.Start = 1; } else if (c >= '0' && c <= '9') { int start = c - '0'; while (pos < len - 1) { c = ln[++pos]; // We limit to 9 digits to avoid overflow, This also seems to be the limit for 'start' in some browsers. if (c >= '0' && c <= '9' && start < 100000000) { start = start * 10 + (c - '0'); } else { break; } } if (pos >= len - 1 || (c != '.' && c != ')')) { return(0); } pos++; if (pos == len || (ln[pos] != ' ' && ln[pos] != '\n')) { return(0); } data = new ListData(); data.ListType = ListType.Ordered; data.BulletChar = '\0'; data.Start = start; data.Delimiter = (c == '.' ? ListDelimiter.Period : ListDelimiter.Parenthesis); } else { return(0); } return(pos - startpos); }