// EXEC: cjc %import-path %L %l %f --test
// EXEC: ./main
package commonmark4cj.commonmark.test
import commonmark4cj.commonmark.*
import commonmark4cj.commonmark.LinkReferenceDefinitionParser as LinkReferenceDefinitionParser0
import std.unittest.*
import std.unittest.testmacro.*
@Test
public class LinkReferenceDefinitionParserTest {
@TestCase
public func testStartLabel(): Unit {
let parser: LinkReferenceDefinitionParser = LinkReferenceDefinitionParser()
parser.parse(SourceLine.of("[", None))
@Expect(parser.getParagraphLines().getContent(), "[")
}
@TestCase
public func testStartNoLabel(): Unit {
let parser: LinkReferenceDefinitionParser = LinkReferenceDefinitionParser()
assertParagraph("a")
parser.parse(SourceLine.of("a", None))
parser.parse(SourceLine.of("[", None))
@Expect("a\n[", parser.getParagraphLines().getContent())
}
@TestCase
public func testEmptyLabel(): Unit {
assertParagraph("[]: /")
assertParagraph("[ ]: /")
assertParagraph("[ \t\n\u{000B}\f\r ]: /")
}
@TestCase
public func testLabelColon(): Unit {
assertParagraph("[foo] : /")
}
@TestCase
public func testLabel(): Unit {
assertState("[foo]:", State.DESTINATION, "[foo]:")
assertState("[ foo ]:", State.DESTINATION, "[ foo ]:")
}
@TestCase
public func testLabelInvalid(): Unit {
assertParagraph("[foo[]:")
}
@TestCase
public func testLabelMultiline(): Unit {
let parser: LinkReferenceDefinitionParser = LinkReferenceDefinitionParser()
parser.parse(SourceLine.of("[two", None))
parser.parse(SourceLine.of("lines]:", None))
parser.parse(SourceLine.of("/url", None))
assertDef(parser.getDefinitions()[0], "two\nlines", "/url", None)
}
@TestCase
public func testLabelStartsWithNewline(): Unit {
let parser: LinkReferenceDefinitionParser = LinkReferenceDefinitionParser()
parser.parse(SourceLine.of("[", None))
parser.parse(SourceLine.of("weird]:", None))
parser.parse(SourceLine.of("/url", None))
assertDef(parser.getDefinitions()[0], "\nweird", "/url", None)
}
@TestCase
public func testDestination(): Unit {
let parser: LinkReferenceDefinitionParser = LinkReferenceDefinitionParser()
parser.parse(SourceLine.of("[foo]: /url", None))
@Expect("", parser.getParagraphLines().getContent())
@Expect(1, parser.getDefinitions().size)
assertDef(parser.getDefinitions()[0], "foo", "/url", None)
parser.parse(SourceLine.of("[bar]: </url2>", None))
assertDef(parser.getDefinitions()[1], "bar", "/url2", None)
}
@TestCase
public func testDestinationInvalid(): Unit {
assertParagraph("[foo]: <bar<>")
}
@TestCase
public func testTitle(): Unit {
let parser: LinkReferenceDefinitionParser = LinkReferenceDefinitionParser()
parser.parse(SourceLine.of("[foo]: /url 'title'", None))
@Expect("", parser.getParagraphLines().getContent())
@Expect(1, parser.getDefinitions().size)
assertDef(parser.getDefinitions()[0], "foo", "/url", "title")
}
@TestCase
public func testTitleStartWhitespace(): Unit {
let parser: LinkReferenceDefinitionParser = LinkReferenceDefinitionParser()
parser.parse(SourceLine.of("[foo]: /url", None))
@Expect("", parser.getParagraphLines().getContent())
parser.parse(SourceLine.of(" ", None))
@Expect(" ", parser.getParagraphLines().getContent())
@Expect(1, parser.getDefinitions().size)
assertDef(parser.getDefinitions()[0], "foo", "/url", None)
}
@TestCase
public func testTitleMultiline(): Unit {
let parser: LinkReferenceDefinitionParser = LinkReferenceDefinitionParser()
parser.parse(SourceLine.of("[foo]: /url 'two", None))
@Expect("[foo]: /url 'two", parser.getParagraphLines().getContent())
@Expect(0, parser.getDefinitions().size)
parser.parse(SourceLine.of("lines", None))
@Expect("[foo]: /url 'two\nlines", parser.getParagraphLines().getContent())
@Expect(0, parser.getDefinitions().size)
parser.parse(SourceLine.of("'", None))
@Expect("", parser.getParagraphLines().getContent())
@Expect(1, parser.getDefinitions().size)
assertDef(parser.getDefinitions()[0], "foo", "/url", "two\nlines\n")
}
@TestCase
public func testTitleMultiline2(): Unit {
let parser: LinkReferenceDefinitionParser = LinkReferenceDefinitionParser()
parser.parse(SourceLine.of("[foo]: /url '", None))
parser.parse(SourceLine.of("title'", None))
assertDef(parser.getDefinitions()[0], "foo", "/url", "\ntitle")
}
@TestCase
public func testTitleInvalid(): Unit {
assertParagraph("[foo]: /url (invalid(")
assertParagraph("[foo]: </url>'title'")
assertParagraph("[foo]: /url 'title' INVALID")
}
private static func assertParagraph(input: String) {
assertState(input, State.PARAGRAPH, input)
}
private static func assertState(input: String, state: State, paragraphContent: String) {
let parser: LinkReferenceDefinitionParser = LinkReferenceDefinitionParser()
parser.parse(SourceLine.of(input, None))
@Expect(paragraphContent, parser.getParagraphLines().getContent())
}
private static func assertDef(def: LinkReferenceDefinition, label: String, destination: String, title: ?String) {
@Expect(label, def.getLabel())
@Expect(destination, def.getDestination())
@Expect(title, def.getTitle())
}
}
/**
* Parser for link reference definitions at the beginning of a paragraph.
*/
public class LinkReferenceDefinitionParser {
private var state: State = State.START_DEFINITION
private var paragraphLines: ArrayList<SourceLine> = ArrayList()
private var definitions: ArrayList<LinkReferenceDefinition> = ArrayList<LinkReferenceDefinition>()
private let sourceSpans: ArrayList<SourceSpan> = ArrayList()
private var label: ?StringBuilder = None
private var destination: String = ""
private var titleDelimiter: Byte = 0
private var title: ?StringBuilder = None
private var referenceValid: Bool = false
public func parse(line: SourceLine): Unit {
paragraphLines.add(line)
if (state == State.PARAGRAPH) {
// We're in a paragraph now. Link reference definitions can only appear at the beginning, so once
// we're in a paragraph, there's no going back.
return
}
let scanner: Scanner = Scanner.of(SourceLines.of(line))
var success = false
while (scanner.hasNext()) {
match (state) {
case START_DEFINITION => success = startDefinition(scanner)
case LABEL => success = labelFunc(scanner)
case DESTINATION => success = destinationFunc(scanner)
case START_TITLE => success = startTitle(scanner)
case TITLE => success = titleFunc(scanner)
case _ => throw IllegalStateException("Unknown parsing state: ${state.toString()}")
}
// Parsing failed, which means we fall back to treating text as a paragraph.
if (!success) {
state = State.PARAGRAPH
// If parsing of the title part failed, we still have a valid reference that we can add, and we need to
// do it before the source span for this line is added.
finishReference()
return
}
}
}
public func addSourceSpan(sourceSpan: SourceSpan): Unit {
sourceSpans.add(sourceSpan)
}
/**
* @return the lines that are normal paragraph content, without newlines
*/
func getParagraphLines(): SourceLines {
return SourceLines.of(paragraphLines)
}
@Frozen
func getParagraphSourceSpans(): ArrayList<SourceSpan> {
return sourceSpans
}
@Frozen
func getDefinitions(): ArrayList<LinkReferenceDefinition> {
finishReference()
return definitions
}
public func getState(): State {
return state
}
func removeLines(lines: Int): ArrayList<SourceSpan> {
var removedSpans = sourceSpans[max(sourceSpans.size - lines, 0)..sourceSpans.size]
removeLast(lines, paragraphLines)
removeLast(lines, sourceSpans)
return removedSpans
}
private func startDefinition(scanner: Scanner): Bool {
// Finish any outstanding references now. We don't do this earlier because we need addSourceSpan to have been
// called before we do it.
finishReference()
scanner.whitespace()
if (!scanner.next('[')) {
return false
}
state = State.LABEL
label = StringBuilder()
if (!scanner.hasNext()) {
label?.append('\n')
}
return true
}
private func labelFunc(scanner: Scanner): Bool {
let start: SourcePosition = scanner.position()
if (!LinkScanner.scanLinkLabelContent(scanner)) {
return false
}
label?.append(scanner.getSource(start, scanner.position()).getContent())
if (!scanner.hasNext()) {
// label might continue on next line
label?.append('\n')
return true
} else if (scanner.next(']')) {
// end of label
if (!scanner.next(':')) {
return false
}
// spec: A link label can have at most 999 characters inside the square brackets.
if ((label?.size ?? 0) > 999) {
return false
}
let normalizedLabel: String = Escaping.normalizeLabelContent(label?.toString() ?? "")
if (normalizedLabel.isEmpty()) {
return false
}
state = State.DESTINATION
scanner.whitespace()
return true
} else {
return false
}
}
private func destinationFunc(scanner: Scanner): Bool {
scanner.whitespace()
let start: SourcePosition = scanner.position()
if (!LinkScanner.scanLinkDestination(scanner)) {
return false
}
let rawDestination: String = scanner.getSource(start, scanner.position()).getContent()
destination = if (rawDestination.startsWith("<")) {
rawDestination[1..rawDestination.size - 1]
} else {
rawDestination
}
let whitespace: Int = scanner.whitespace()
if (!scanner.hasNext()) {
// Destination was at end of line, so this is a valid reference for sure (and maybe a title).
// If not at end of line, wait for title to be valid first.
referenceValid = true
paragraphLines.clear()
} else if (whitespace == 0) {
// spec: The title must be separated from the link destination by whitespace
return false
}
state = State.START_TITLE
return true
}
private func startTitle(scanner: Scanner): Bool {
scanner.whitespace()
if (!scanner.hasNext()) {
state = State.START_DEFINITION
return true
}
titleDelimiter = 0
let c: Byte = scanner.peek()
match (c) {
case b'"' | b'\'' => titleDelimiter = c
case b'(' => titleDelimiter = b')'
case _ => ()
}
if (titleDelimiter != 0) {
state = State.TITLE
title = StringBuilder()
scanner.next()
if (!scanner.hasNext()) {
title?.append('\n')
}
} else {
// There might be another reference instead, try that for the same character.
state = State.START_DEFINITION
}
return true
}
private func titleFunc(scanner: Scanner): Bool {
let start: SourcePosition = scanner.position()
if (!LinkScanner.scanLinkTitleContent(scanner, titleDelimiter)) {
// Invalid title, stop. Title collected so far must not be used.
title = None
return false
}
title?.append(scanner.getSource(start, scanner.position()).getContent())
if (!scanner.hasNext()) {
// Title ran until the end of line, so continue on next line (until we find the delimiter)
title?.append('\n')
return true
}
// Skip delimiter character
scanner.next()
scanner.whitespace()
if (scanner.hasNext()) {
// spec: No further non-whitespace characters may occur on the line.
// Title collected so far must not be used.
title = None
return false
}
referenceValid = true
paragraphLines.clear()
// See if there's another definition.
state = State.START_DEFINITION
return true
}
private func finishReference(): Unit {
if (!referenceValid) {
return
}
var d: String = Escaping.unescapeString(destination)
let t: ?String = if (let Some(title) <- title) {
Escaping.unescapeString(title.toString())
} else {
None
}
let definition = LinkReferenceDefinition(label.getOrThrow().toString(), d, t)
definition.setSourceSpans(sourceSpans)
sourceSpans.clear()
definitions.add(definition)
label = None
referenceValid = false
destination = ""
title = None
}
private static func removeLast<T>(n: Int, list: ArrayList<T>): Unit {
if (n >= list.size) {
list.clear()
} else {
list.remove(list.size - n..list.size)
}
}
}