public static void Finalize(Block b, LineInfo line) { // don't do anything if the block is already closed if (!b.IsOpen) { return; } b.IsOpen = false; if (line.IsTrackingPositions) { // HTML Blocks other than type 7 call Finalize when the last line is encountered. // Block types 6 and 7 calls Finalize once it finds the next empty row but that empty row is no longer considered to be part of the block. var includesThisLine = b.HtmlBlockType != HtmlBlockType.None && b.HtmlBlockType != HtmlBlockType.InterruptingBlock && b.HtmlBlockType != HtmlBlockType.NonInterruptingBlock; // (b.SourcePosition >= line.LineOffset) determines if the block started on this line. includesThisLine = includesThisLine || b.SourcePosition >= line.LineOffset; if (includesThisLine && line.Line != null) { b.SourceLastPosition = line.CalculateOrigin(line.Line.Length, false); } else { b.SourceLastPosition = line.CalculateOrigin(0, false); } } switch (b.Tag) { case BlockTag.Paragraph: var sc = b.StringContent; if (!sc.StartsWith('[')) { break; } var subj = new Subject(b.Top.Document); sc.FillSubject(subj); var origPos = subj.Position; while (subj.Position < subj.Buffer.Length && subj.Buffer[subj.Position] == '[' && 0 != InlineMethods.ParseReference(subj)) { } if (subj.Position != origPos) { sc.Replace(subj.Buffer, subj.Position, subj.Buffer.Length - subj.Position); if (sc.PositionTracker != null) { sc.PositionTracker.AddBlockOffset(subj.Position - origPos); } if (Utilities.IsFirstLineBlank(subj.Buffer, subj.Position)) { b.Tag = BlockTag.ReferenceDefinition; } } break; case BlockTag.IndentedCode: b.StringContent.RemoveTrailingBlankLines(); break; case BlockTag.FencedCode: // first line of contents becomes info var firstlinelen = b.StringContent.IndexOf('\n') + 1; b.FencedCodeData.Info = InlineMethods.Unescape(b.StringContent.TakeFromStart(firstlinelen, true).Trim()); break; case BlockTag.List: // determine tight/loose status b.ListData.IsTight = true; // tight by default var item = b.FirstChild; Block subitem; while (item != null) { // check for non-final non-empty list item ending with blank line: if (item.IsLastLineBlank && item.NextSibling != null) { b.ListData.IsTight = false; break; } // recurse into children of list item, to see if there are spaces between them: subitem = item.FirstChild; while (subitem != null) { if (EndsWithBlankLine(subitem) && (item.NextSibling != null || subitem.NextSibling != null)) { b.ListData.IsTight = false; break; } subitem = subitem.NextSibling; } if (!b.ListData.IsTight) { break; } item = item.NextSibling; } break; } }
// 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 adjustment to the virtual position `column` that points to the number of spaces from the TAB that have not been included in any indent. var remainingSpaces = 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; bool all_matched = true; Block cur = curptr; char curChar; int indent; // container starts at the document root. var container = cur.Top; bool blank; // 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 + remainingSpaces; blank = curChar == '\n'; switch (container.Tag) { case BlockTag.BlockQuote: { if (indent <= 3 && curChar == '>') { AdvanceOffset(ln, indent + 1, true, ref offset, ref column, ref remainingSpaces); AdvanceOptionalSpace(ln, ref offset, ref column, ref remainingSpaces); } 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, ref remainingSpaces); } 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, ref remainingSpaces); } else { all_matched = false; } break; } case BlockTag.IndentedCode: { if (indent >= CODE_INDENT) { AdvanceOffset(ln, CODE_INDENT, true, ref offset, ref column, ref remainingSpaces); } else if (blank) { AdvanceOffset(ln, first_nonspace - offset, false, ref offset, ref column, ref remainingSpaces); } 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.Meta: // -1 means we've seen closer if (container.MetaData.FenceLength == -1) { 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; 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 && container.Tag != BlockTag.Meta) { FindFirstNonspace(ln, offset, column, out first_nonspace, out first_nonspace_column, out curChar); indent = first_nonspace_column - column + remainingSpaces; blank = curChar == '\n'; var indented = indent >= CODE_INDENT; if (!indented && curChar == '>') { AdvanceOffset(ln, first_nonspace + 1 - offset, false, ref offset, ref column, ref remainingSpaces); AdvanceOptionalSpace(ln, ref offset, ref column, ref remainingSpaces); container = CreateChildBlock(container, line, BlockTag.BlockQuote, first_nonspace); } else if (!indented && curChar == '#' && 0 != (matched = Scanner.ScanAtxHeadingStart(ln, first_nonspace, ln.Length, out int level))) { AdvanceOffset(ln, first_nonspace + matched - offset, false, ref offset, ref column, ref remainingSpaces); container = CreateChildBlock(container, line, BlockTag.AtxHeading, first_nonspace); container.Heading = new HeadingData(level); } else if (!indented && (curChar == '-') && 3 == (matched = Scanner.ScanMetaDataOpen(ln, first_nonspace, ln.Length))) { container = CreateChildBlock(container, line, BlockTag.Meta, first_nonspace); container.MetaData = new MetaData() { FenceLength = matched }; AdvanceOffset(ln, first_nonspace + matched - offset, false, ref offset, ref column, ref remainingSpaces); } else if (!indented && (curChar == '`' || curChar == '~') && 0 != (matched = Scanner.ScanOpenCodeFence(ln, first_nonspace, ln.Length))) { container = CreateChildBlock(container, line, BlockTag.FencedCode, first_nonspace); container.FencedCodeData = new FencedCodeData { FenceChar = curChar, FenceLength = matched, FenceOffset = first_nonspace - offset }; AdvanceOffset(ln, first_nonspace + matched - offset, false, ref offset, ref column, ref remainingSpaces); } else if (!indented && curChar == '<' && (0 != (matched = (int)Scanner.ScanHtmlBlockStart(ln, first_nonspace, ln.Length)) || (container.Tag != BlockTag.Paragraph && 0 != (matched = (int)Scanner.ScanHtmlBlockStart7(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.ScanSetextHeadingStart(ln, first_nonspace, ln.Length))) { container.Tag = BlockTag.SetextHeading; container.Heading = new HeadingData(matched); AdvanceOffset(ln, ln.Length - 1 - offset, false, ref offset, ref column, ref remainingSpaces); } else if (!indented && !(container.Tag == BlockTag.Paragraph && !all_matched) && 0 != Scanner.ScanThematicBreak(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, ref remainingSpaces); } else if ((!indented || container.Tag == BlockTag.List) && 0 != (matched = ParseListMarker(ln, first_nonspace, container.Tag == BlockTag.Paragraph, out ListData data))) { // compute padding: AdvanceOffset(ln, first_nonspace + matched - offset, false, ref offset, ref column, ref remainingSpaces); var prevOffset = offset; var prevColumn = column; var prevRemainingSpaces = remainingSpaces; while (column - prevColumn <= CODE_INDENT) { if (!AdvanceOptionalSpace(ln, ref offset, ref column, ref remainingSpaces)) { break; } } // i = number of spaces after marker, up to 5 if (column == prevColumn) { // no spaces at all data.Padding = matched + 1; } else if (column - prevColumn > CODE_INDENT || ln[offset] == '\n') { data.Padding = matched + 1; // too many (or none) spaces, ignoring everything but the first one offset = prevOffset; column = prevColumn; remainingSpaces = prevRemainingSpaces; AdvanceOptionalSpace(ln, ref offset, ref column, ref remainingSpaces); } else { data.Padding = matched + column - prevColumn; } // 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, ref remainingSpaces); 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.Meta && !(container.Tag == BlockTag.ListItem && container.FirstChild is 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, remainingSpaces); } 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 is null) { throw new PandocMarkException("Cannot finalize container block. Last matched container tag = " + last_matched_container.Tag); } } if (container.Tag == BlockTag.IndentedCode) { AddLine(container, line, ln, offset, remainingSpaces); } else if (container.Tag == BlockTag.Meta) { if ((indent <= 3 && curChar == '.') && (0 != Scanner.ScanCloseMetaData(ln, first_nonspace, container.MetaData.FenceLength, ln.Length))) { // if closing fence, set fence length to -1. it will be closed when the next line is processed. container.MetaData.FenceLength = -1; } else if (!ln.StartsWith("---")) { AddMetaDataEntry(container, line, ln, offset, remainingSpaces); } } else if (container.Tag == BlockTag.FencedCode) { if ((indent <= 3 && curChar == container.FencedCodeData.FenceChar) && (0 != Scanner.ScanCloseCodeFence(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, remainingSpaces); } } else if (container.Tag == BlockTag.HtmlBlock) { AddLine(container, line, ln, offset, remainingSpaces); if (Scanner.ScanHtmlBlockEnd(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] == '\t' || ln[p] == '\n')) { p--; } int px = 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] != ' ' && ln[p] != '\t')) { p = px; } // trim trailing spaces that are before the closing #s while (p >= first_nonspace && (ln[p] == ' ' || ln[p] == '\t')) { p--; } AddLine(container, line, ln, first_nonspace, remainingSpaces, p - first_nonspace + 1); Finalize(container, line); container = container.Parent; } else if (AcceptsLines(container.Tag)) { AddLine(container, line, ln, first_nonspace, remainingSpaces, isAddOffsetRequired: container.Parent is null || container.Parent.Tag == BlockTag.Document); } 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, remainingSpaces); } else { Utilities.Warning("Line {0} with container type {1} did not match any condition:\n\"{2}\"", line.LineNumber, container.Tag, ln); } curptr = container; } }
public void AddOffset(LineInfo line, int startIndex, int length) { if (this.OffsetCount + line.OffsetCount + 2 >= this.Offsets.Length) { Array.Resize(ref this.Offsets, this.Offsets.Length + line.OffsetCount + 20); } PositionOffset po1, po2; if (startIndex > 0) { po1 = new PositionOffset( CalculateOrigin(line.Offsets, line.OffsetCount, line.LineOffset, false, true), startIndex); } else { po1 = EmptyPositionOffset; } if (line.Line.Length - startIndex - length > 0) { po2 = new PositionOffset( CalculateOrigin(line.Offsets, line.OffsetCount, line.LineOffset + startIndex + length, false, true), line.Line.Length - startIndex - length); } else { po2 = EmptyPositionOffset; } var indexAfterLastCopied = 0; if (po1.Offset == 0) { if (po2.Offset == 0) { goto FINTOTAL; } po1 = po2; po2 = EmptyPositionOffset; } for (var i = 0; i < line.OffsetCount; i++) { var pc = line.Offsets[i]; if (pc.Position > po1.Position) { if (i > indexAfterLastCopied) { Array.Copy(line.Offsets, indexAfterLastCopied, this.Offsets, this.OffsetCount, i - indexAfterLastCopied); this.OffsetCount += i - indexAfterLastCopied; indexAfterLastCopied = i; } this.Offsets[this.OffsetCount++] = po1; po1 = po2; if (po1.Offset == 0) { goto FIN; } po2 = EmptyPositionOffset; } } FIN: if (po1.Offset != 0) { this.Offsets[this.OffsetCount++] = po1; } if (po2.Offset != 0) { this.Offsets[this.OffsetCount++] = po2; } FINTOTAL: Array.Copy(line.Offsets, indexAfterLastCopied, this.Offsets, this.OffsetCount, line.OffsetCount - indexAfterLastCopied); this.OffsetCount += line.OffsetCount - indexAfterLastCopied; }