/*
* Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved.
*/
package commonmark4cj.commonmark
class DocumentParser <: ParserState {
private static var CORE_FACTORY_TYPES: HashSet<NodeType> = HashSet<NodeType>(
[
BlockQuoteType,
HeadingType,
FencedCodeBlockType,
HtmlBlockType,
ThematicBreakType,
ListBlockType,
IndentedCodeBlockType
]
)
private static var NODES_TO_CORE_FACTORIES: HashMap<NodeType, BlockParserFactory> = HashMap<NodeType, BlockParserFactory>(
[
(BlockQuoteType, BlockQuoteParserFactory()),
(HeadingType, HeadingParserFactory()),
(FencedCodeBlockType, FencedCodeBlockParserFactory()),
(HtmlBlockType, HtmlBlockParserFactory()),
(ThematicBreakType, ThematicBreakParserFactory()),
(ListBlockType, ListBlockParserFactory()),
(IndentedCodeBlockType, IndentedCodeBlockParserFactory())
]
)
private var line: SourceLine = SourceLine.of("", None)
/**
* Line index (0-based)
*/
private var lineIndex = -1
/**
* current index (offset) in input line (0-based)
*/
private var index: Int64 = 0
/**
* current column of input line (tab causes column to go to next 4-space tab stop) (0-based)
*/
private var column: Int64 = 0
/**
* if the current column is within a tab character (partially consumed tab)
*/
private var columnIsInTab: Bool = false
private var nextNonSpace: Int64 = 0
private var nextNonSpaceColumn: Int64 = 0
private var indent: Int64 = 0
private var blank: Bool = false
private var blockParserFactories: ArrayList<BlockParserFactory>
private var inlineParserFactory: InlineParserFactory
private var delimiterProcessors: ArrayList<DelimiterProcessor>
private var documentBlockParser: DocumentBlockParser
// private var definitions: HashMap<String, LinkReferenceDefinition> = HashMap<String, LinkReferenceDefinition>()
private var definitions = Definitions()
private var openBlockParsers: ArrayList<OpenBlockParser> = ArrayList()
// LinkedHashSet to have a deterministic order
private var allBlockParsers: ArrayList<AbstractBlockParser> = ArrayList()
// private var allBlockParsers: HashSet<AbstractBlockParser> = HashSet<AbstractBlockParser>()
private let inlineParserContext: InlineParserContext
private let includeSourceSpans: IncludeSourceSpans
public init(
blockParserFactories: ArrayList<BlockParserFactory>,
inlineParserFactory: InlineParserFactory,
delimiterProcessors: ArrayList<DelimiterProcessor>,
inlineParserContext: InlineParserContext,
includeSourceSpans: IncludeSourceSpans
) {
this.blockParserFactories = blockParserFactories
this.inlineParserFactory = inlineParserFactory
this.delimiterProcessors = delimiterProcessors
this.inlineParserContext = inlineParserContext
this.includeSourceSpans = includeSourceSpans
this.documentBlockParser = DocumentBlockParser()
activateBlockParser(OpenBlockParser(this.documentBlockParser, 0))
}
@Frozen
public static func getDefaultBlockParserTypes(): HashSet<NodeType> {
return CORE_FACTORY_TYPES
}
public static func calculateBlockParserFactories(
customBlockParserFactories: ArrayList<BlockParserFactory>,
enabledBlockTypes: HashSet<NodeType>
): ArrayList<BlockParserFactory> {
var list: ArrayList<BlockParserFactory> = ArrayList<BlockParserFactory>()
// By having the custom factories come first, extensions are able to change behavior of core syntax.
list.add(all: customBlockParserFactories)
for (blockType in enabledBlockTypes) {
list.add(NODES_TO_CORE_FACTORIES.get(blockType).getOrThrow())
}
return list
}
/**
* The main parsing function. Returns a parsed document AST.
*/
public func parse(input: String): Document {
this.input = input
lineStart = 0
lineBreak = Characters.findLineBreak(input, lineStart)
while (lineBreak != -1) {
var line = input[lineStart..lineBreak]
let lineStartOld = lineStart
// 预处理lineStart/lineBreak 便于getNextLine
if (lineBreak + 1 < input.size && input[lineBreak] == b'\r' && input[lineBreak + 1] == b'\n') {
lineStart = lineBreak + 2
} else {
lineStart = lineBreak + 1
}
lineBreak = Characters.findLineBreak(input, lineStart)
parseLine(line, lineStartOld)
}
if (input.size > 0 && (lineStart == 0 || lineStart < input.size)) {
var line = input[lineStart..input.size]
parseLine(line, lineStart)
}
return finalizeAndProcess()
}
@Frozen
public func parse(input: InputStream): Document {
lineStart = 0
let reader = StringLineReader(input)
while (let Some((line, lbs)) <- reader.readln()) {
parseLine(line, lineStart)
lineStart += line.size
lineStart += lbs
}
return finalizeAndProcess()
}
public func getLine(): SourceLine {
return line
}
private var input: String = "" // 所有行的内容
private var lineStart: Int = -1
private var lineBreak: Int = -1
public func getNextLine(): String {
if (lineStart <= lineBreak) {
input[lineStart..lineBreak]
} else {
""
}
}
public func getIndex(): Int64 {
return index
}
public func getNextNonSpaceIndex(): Int64 {
return nextNonSpace
}
public func getColumn(): Int64 {
return column
}
public func getIndent(): Int64 {
return indent
}
public func isBlank(): Bool {
return blank
}
@Frozen
public func getActiveBlockParser(): AbstractBlockParser {
return openBlockParsers[openBlockParsers.size - 1].blockParser
}
private func setLine(ln: String, inputIndex: Int): Unit {
lineIndex++
index = 0
column = 0
columnIsInTab = false
let lineContent = prepareLine(ln)
var sourceSpan: ?SourceSpan = None
if (includeSourceSpans.need) {
sourceSpan = SourceSpan.of(lineIndex, 0, inputIndex, lineContent.size)
}
this.line = SourceLine.of(lineContent, sourceSpan)
}
/**
* Analyze a line of text and update the document appropriately. We parse markdown text by calling this on each
* line of input, then finalizing the document.
*/
@Frozen
private func parseLine(ln: String, inputIndex: Int): Unit {
setLine(ln, inputIndex)
// For each containing block, try to parse the associated line start.
// Bail out on failure: container will poInt64 to the last matching block.
// Set all_matched to false if not all containers match.
// The document will always match, can be skipped
var matches: Int64 = 1
for (i in 1..openBlockParsers.size) {
let openBlockParser = openBlockParsers[i]
let blockParser = openBlockParser.blockParser
findNextNonSpace()
if (let Some(blockContinue: BlockContinueImpl) <- blockParser.tryContinue(this)) {
openBlockParser.sourceIndex = getIndex()
if (blockContinue.isFinalize()) {
addSourceSpans()
closeBlockParsers(openBlockParsers.size - i)
return
} else {
if (blockContinue.getNewIndex() != -1) {
setNewIndex(blockContinue.getNewIndex())
} else if (blockContinue.getNewColumn() != -1) {
setNewColumn(blockContinue.getNewColumn())
}
matches++
}
} else {
break
}
}
var unmatchedBlocks: Int64 = openBlockParsers.size - matches
var blockParser = openBlockParsers[matches - 1].blockParser
var startedNewBlock = false
var lastIndex: Int = index
// Unless last matched container is a code block, try container starts,
// adding children to the last matched container:
var tryBlockStarts: Bool = blockParser.getBlock() is Paragraph || blockParser.isContainer()
while (tryBlockStarts) {
lastIndex = index
findNextNonSpace()
if (isBlank() || (indent < Parsing.CODE_BLOCK_INDENT && Characters.isLetter(line.getContent(), nextNonSpace))) {
setNewIndex(nextNonSpace)
break
}
var blockStart: BlockStartImpl = if (let Some(v) <- findBlockStart(blockParser)) {
v
} else {
setNewIndex(nextNonSpace)
break
}
startedNewBlock = true
var sourceIndex = getIndex()
// We're starting a new block. If we have any previous blocks that need to be closed, we need to do it now.
if (unmatchedBlocks > 0) {
closeBlockParsers(unmatchedBlocks)
unmatchedBlocks = 0
}
if (blockStart.getNewIndex() != -1) {
setNewIndex(blockStart.getNewIndex())
} else if (blockStart.getNewColumn() != -1) {
setNewColumn(blockStart.getNewColumn())
}
var replacedSourceSpans: ?ArrayList<SourceSpan> = None
if (blockStart.getReplaceParagraphLines() >= 1 || blockStart.isReplaceActiveBlockParser()) {
var activeBlockParser = getActiveBlockParser()
if (let Some(paragraphParser: ParagraphParser) <- (activeBlockParser as ParagraphParser)) {
var lines = if (blockStart.isReplaceActiveBlockParser()) {
Int64.Max
} else {
blockStart.getReplaceParagraphLines()
}
replacedSourceSpans = replaceParagraphLines(lines, paragraphParser)
} else if (blockStart.isReplaceActiveBlockParser()) {
replacedSourceSpans = prepareActiveBlockParserForReplacement(activeBlockParser)
}
}
for (newBlockParser in blockStart.getBlockParsers()) {
addChild(OpenBlockParser(newBlockParser, sourceIndex))
if (let Some(v) <- replacedSourceSpans) {
newBlockParser.getBlock().setSourceSpans(v)
}
blockParser = newBlockParser
tryBlockStarts = newBlockParser.isContainer()
}
}
// What remains at the offset is a text line. Add the text to the
// appropriate block.
// First check for a lazy paragraph continuation:
if (!startedNewBlock && !isBlank() && getActiveBlockParser().canHaveLazyContinuationLines()) { /*cjlint-ignore !G.EXP.03 */
// lazy paragraph continuation
addLine()
} else {
// finalize any blocks not matched
if (unmatchedBlocks > 0) {
closeBlockParsers(unmatchedBlocks)
}
if (!blockParser.isContainer()) {
addLine()
} else if (!isBlank()) {
// create paragraph container for line
addChild(OpenBlockParser(ParagraphParser(), lastIndex))
addLine()
} else {
// This can happen for a list item like this:
// ```
// *
// list item
// ```
//
// The first line does not start a paragraph yet, but we still want to record source positions.
addSourceSpans()
}
}
}
private func findNextNonSpace(): Unit {
var i: Int64 = index
var colsNum: Int64 = column
blank = true
var length: Int64 = line.getContent().size
while (i < length) {
var c = line.getContent()[i]
if (c == b' ') {
i++
colsNum++
continue
} else if (c == b'\t') {
i++
colsNum += (4 - (colsNum % 4))
continue
}
blank = false
break
}
nextNonSpace = i
nextNonSpaceColumn = colsNum
indent = nextNonSpaceColumn - column
}
private func setNewIndex(newIndex: Int64): Unit {
if (newIndex >= nextNonSpace) {
// We can start from here, no need to calculate tab stops again
index = nextNonSpace
column = nextNonSpaceColumn
}
var length: Int64 = line.getContent().size
while (index < newIndex && index != length) {
advance()
}
// If we're going to an index as opposed to a column, we're never within a tab
columnIsInTab = false
}
private func setNewColumn(newColumnNum: Int64): Unit {
if (newColumnNum >= nextNonSpaceColumn) {
// We can start from here, no need to calculate tab stops again
index = nextNonSpace
column = nextNonSpaceColumn
}
var length: Int64 = line.getContent().size
while (column < newColumnNum && index != length) {
advance()
}
if (column > newColumnNum) {
// Last character was a tab and we overshot our target
index--
column = newColumnNum
columnIsInTab = true
} else {
columnIsInTab = false
}
}
/**
* append contents
*/
private func addLine(): Unit {
var content: String
if (columnIsInTab) {
var afterTab: Int64 = index + 1
var rest = line.getContent()[afterTab..line.getContent().size]
var spaces: Int64 = Parsing.columnsToNextTabStop(column)
var sb: StringBuilder = StringBuilder(spaces + rest.size)
sb.append(" " * spaces)
sb.append(rest)
content = sb.toString()
} else if (index == 0) {
content = line.getContent()
} else {
content = line.getContent()[index..line.getContent().size]
}
var sourceSpan: ?SourceSpan = None
if (includeSourceSpans == IncludeSourceSpans.BLOCKS_AND_INLINES && index < (line.getSourceSpan()?.getLength() ??
-1)) {
// Note that if we're in a partially-consumed tab the length of the source span and the content don't match.
sourceSpan = line.getSourceSpan()?.subSpan(index)
}
getActiveBlockParser().addLine(SourceLine.of(content, sourceSpan))
addSourceSpans()
}
private func addSourceSpans(): Unit {
if (includeSourceSpans.need) {
// Don't add source spans for Document itself (it would get the whole source text), so start at 1, not 0
for (i in 1..openBlockParsers.size) {
var openBlockParser = openBlockParsers[i]
// In case of a lazy continuation line, the index is less than where the block parser would expect the
// contents to start, so let's use whichever is smaller.
let blockIndex = min(openBlockParser.sourceIndex, index)
let length = line.getContent().size - blockIndex
if (let Some(ss) <- line.getSourceSpan() && length != 0) {
openBlockParser.blockParser.addSourceSpan(ss.subSpan(blockIndex))
}
}
}
}
private func advance(): Unit {
var lineChar = line.getContent()[index]
if (lineChar == b'\t') {
index++
column = Parsing.columnsToNextTabStop(column) + column
} else {
index++
column++
}
}
@Frozen
private func findBlockStart(blockParser: AbstractBlockParser): Option<BlockStartImpl> {
var matchedBlockParser: MatchedBlockParser = MatchedBlockParserImpl(blockParser)
for (blockParserFactory in blockParserFactories) {
var result: ?BlockStart = blockParserFactory.tryStart(this, matchedBlockParser)
if (let Some(v: BlockStartImpl) <- result) {
return v
}
}
return None
}
/**
* Finalize a block. Close it and do any necessary postprocessing, e.g. creating string_content from strings,
* setting the 'tight' or 'loose' status of a list, and parsing the beginnings of paragraphs for reference
* definitions.
*/
private func finalize(blockParser: BlockParser): Unit {
addDefinitionsFrom(blockParser)
blockParser.closeBlock()
}
private func addDefinitionsFrom(blockParser: BlockParser): Unit {
for (ref in blockParser.getDefinitions()) {
definitions.addDefinition(ref)
}
}
/**
* Prepares the input line replacing {@code \0}
*/
private static func prepareLine(line: String): String {
line.replace("\0", "\u{FFFD}")
}
/**
* Walk through a block & children recursively, parsing string content into inline content where appropriate.
*/
private func processInlines(): Unit {
var context = InlineParserContextImpl(
inlineParserContext.getCustomInlineContentParserFactories(),
delimiterProcessors,
inlineParserContext.getCustomLinkProcessors(),
inlineParserContext.getCustomLinkMarkers(),
definitions,
inlineParserContext.getDelimiterOpenCloseProcessor()
)
if (let Some(ctx) <- (this.inlineParserContext as InlineParserContextImpl)) {
ctx.calc()
context.ctxCalc = ctx.ctxCalc
}
var inlineParser: InlineParser = inlineParserFactory.create(context)
for (blockParser in allBlockParsers) {
blockParser.parseInlines(inlineParser)
}
}
/**
* Add block of type tag as a child of the tip. If the tip can't accept children, close and finalize it and try
* its parent, and so on til we find a block that can accept children.
*/
private func addChild(openBlockParser: OpenBlockParser): Unit {
while (!getActiveBlockParser().canContain(openBlockParser.blockParser.getBlock())) {
closeBlockParsers(1)
}
getActiveBlockParser().getBlock().appendChild(openBlockParser.blockParser.getBlock())
activateBlockParser(openBlockParser)
}
@Frozen
private func activateBlockParser(openBlockParser: OpenBlockParser): Unit {
openBlockParsers.add(openBlockParser)
}
private func deactivateBlockParser(): OpenBlockParser {
openBlockParsers.remove(at: openBlockParsers.size - 1)
}
private func replaceParagraphLines(lines: Int, paragraphParser: ParagraphParser): ArrayList<SourceSpan> {
// Remove lines from paragraph as the new block is using them.
// If all lines are used, this also unlinks the Paragraph block.
var sourceSpans = paragraphParser.removeLines(lines)
// Close the paragraph block parser, which will finalize it.
closeBlockParsers(1)
return sourceSpans
}
private func prepareActiveBlockParserForReplacement(blockParser: BlockParser): ArrayList<SourceSpan> {
// Note that we don't want to parse inlines here, as it's getting replaced.
deactivateBlockParser()
// Do this so that source positions are calculated, which we will carry over to the replacing block.
blockParser.closeBlock()
blockParser.getBlock().unlink()
return blockParser.getBlock().getSourceSpans()
}
private func finalizeAndProcess(): Document {
closeBlockParsers(openBlockParsers.size)
processInlines()
return documentBlockParser.getBlock()
}
@Frozen
private func closeBlockParsers(count: Int): Unit {
for (_ in 0..count) {
let blockParser = deactivateBlockParser().blockParser
finalize(blockParser)
// Remember for inline parsing. Note that a lot of blocks don't need inline parsing. We could have a
// separate interface (e.g. BlockParserWithInlines) so that we only have to remember those that actually
// have inlines to parse.
allBlockParsers.add(blockParser)
}
}
}
class MatchedBlockParserImpl <: MatchedBlockParser {
private var matchedBlockParser: AbstractBlockParser
public MatchedBlockParserImpl(matchedBlockParser: AbstractBlockParser) {
this.matchedBlockParser = matchedBlockParser
}
public func getMatchedBlockParser(): AbstractBlockParser {
return matchedBlockParser
}
public func getParagraphLines(): SourceLines {
match (matchedBlockParser) {
case v: ParagraphParser => v.getParagraphLines()
case _ => SourceLines.empty()
}
}
}
class OpenBlockParser {
OpenBlockParser(let blockParser: AbstractBlockParser, var sourceIndex: Int) {}
}