diff --git a/src/Markdig.Tests/Specs/DefinitionListSpecs.generated.cs b/src/Markdig.Tests/Specs/DefinitionListSpecs.generated.cs new file mode 100644 index 0000000..b76cf50 --- /dev/null +++ b/src/Markdig.Tests/Specs/DefinitionListSpecs.generated.cs @@ -0,0 +1,195 @@ + +// -------------------------------- +// Definition Lists +// -------------------------------- + +using System; +using NUnit.Framework; + +namespace Markdig.Tests.Specs.DefinitionLists +{ + [TestFixture] + public class TestExtensionsDefinitionLists + { + // # Extensions + // + // This section describes the different extensions supported: + // + // ## Definition lists + // + // A custom container is similar to a fenced code block, but it is using the character `:` to declare a block (with at least 3 characters), and instead of generating a `
...
` it will generate a `
...
` block. + [Test] + public void ExtensionsDefinitionLists_Example001() + { + // Example 1 + // Section: Extensions / Definition lists + // + // The following Markdown: + // + // Term 1 + // : This is a definition item + // With a paragraph + // > This is a block quote + // + // - This is a list + // - with an item2 + // + // ```java + // Test + // + // + // ``` + // + // And a last line + // : This ia another definition item + // + // Term2 + // Term3 *with some inline* + // : This is another definition for term2 + // + // Should be rendered as: + //
+ //
Term 1
+ //

This is a definition item + // With a paragraph

+ //
+ //

This is a block quote

+ //
+ // + //
Test
+            //     
+            //     
+            //     
+ //

And a last line

+ //
+ //
This ia another definition item
+ //
Term2
+ //
Term3 with some inline
+ //
This is another definition for term2
+ //
+ + TestParser.TestSpec("\nTerm 1\n: This is a definition item\n With a paragraph\n > This is a block quote\n\n - This is a list\n - with an item2\n\n ```java\n Test\n\n\n ```\n\n And a last line\n: This ia another definition item\n\nTerm2\nTerm3 *with some inline*\n: This is another definition for term2", "
\n
Term 1
\n

This is a definition item\nWith a paragraph

\n
\n

This is a block quote

\n
\n\n
Test\n\n\n
\n

And a last line

\n
\n
This ia another definition item
\n
Term2
\n
Term3 with some inline
\n
This is another definition for term2
\n
", "definitionlists+attributes|advanced", context: "Example 1\nSection Extensions / Definition lists\n"); + } + + // A definition term can be followed at most by one blank line. Lazy continuations are supported for definitions: + [Test] + public void ExtensionsDefinitionLists_Example002() + { + // Example 2 + // Section: Extensions / Definition lists + // + // The following Markdown: + // Term 1 + // + // : Definition + // with lazy continuation. + // + // Second paragraph of the definition. + // + // Should be rendered as: + //
+ //
Term 1
+ //

Definition + // with lazy continuation.

+ //

Second paragraph of the definition.

+ //
+ //
+ + TestParser.TestSpec("Term 1\n\n: Definition\nwith lazy continuation.\n\n Second paragraph of the definition.", "
\n
Term 1
\n

Definition\nwith lazy continuation.

\n

Second paragraph of the definition.

\n
\n
", "definitionlists+attributes|advanced", context: "Example 2\nSection Extensions / Definition lists\n"); + } + + // The definition must be indented to 4 characters including the `:`. + [Test] + public void ExtensionsDefinitionLists_Example003() + { + // Example 3 + // Section: Extensions / Definition lists + // + // The following Markdown: + // Term 1 + // : Invalid with less than 3 characters + // + // Should be rendered as: + //

Term 1 + // : Invalid with less than 3 characters

+ + TestParser.TestSpec("Term 1\n: Invalid with less than 3 characters", "

Term 1\n: Invalid with less than 3 characters

", "definitionlists+attributes|advanced", context: "Example 3\nSection Extensions / Definition lists\n"); + } + + // The `:` can be indented up to 3 spaces: + [Test] + public void ExtensionsDefinitionLists_Example004() + { + // Example 4 + // Section: Extensions / Definition lists + // + // The following Markdown: + // Term 1 + // : Valid even if `:` starts at most 3 spaces + // + // Should be rendered as: + //
+ //
Term 1
+ //
Valid even if : starts at most 3 spaces
+ //
+ + TestParser.TestSpec("Term 1\n : Valid even if `:` starts at most 3 spaces", "
\n
Term 1
\n
Valid even if : starts at most 3 spaces
\n
", "definitionlists+attributes|advanced", context: "Example 4\nSection Extensions / Definition lists\n"); + } + + // But more than 3 spaces before `:` will trigger an indented code block: + [Test] + public void ExtensionsDefinitionLists_Example005() + { + // Example 5 + // Section: Extensions / Definition lists + // + // The following Markdown: + // Term 1 + // + // : Not valid + // + // Should be rendered as: + //

Term 1

+ //
: Not valid
+            //     
+ + TestParser.TestSpec("Term 1\n\n : Not valid", "

Term 1

\n
: Not valid\n
", "definitionlists+attributes|advanced", context: "Example 5\nSection Extensions / Definition lists\n"); + } + + // Definition lists can be nested inside list items + [Test] + public void ExtensionsDefinitionLists_Example006() + { + // Example 6 + // Section: Extensions / Definition lists + // + // The following Markdown: + // 1. First + // + // 2. Second + // + // Term 1 + // : Definition + // + // Term 2 + // : Second Definition + // + // Should be rendered as: + //
    + //
  1. First

  2. + //
  3. Second

    + //
    + //
    Term 1
    + //
    Definition
    + //
    Term 2
    + //
    Second Definition
    + //
  4. + //
+ + TestParser.TestSpec("1. First\n \n2. Second\n \n Term 1\n : Definition\n \n Term 2\n : Second Definition", "
    \n
  1. First

  2. \n
  3. Second

    \n
    \n
    Term 1
    \n
    Definition
    \n
    Term 2
    \n
    Second Definition
    \n
  4. \n
", "definitionlists+attributes|advanced", context: "Example 6\nSection Extensions / Definition lists\n"); + } + } +} diff --git a/src/Markdig.Tests/Specs/DefinitionListSpecs.md b/src/Markdig.Tests/Specs/DefinitionListSpecs.md new file mode 100644 index 0000000..b0b1522 --- /dev/null +++ b/src/Markdig.Tests/Specs/DefinitionListSpecs.md @@ -0,0 +1,132 @@ +# Extensions + +This section describes the different extensions supported: + +## Definition lists + +A custom container is similar to a fenced code block, but it is using the character `:` to declare a block (with at least 3 characters), and instead of generating a `
...
` it will generate a `
...
` block. + +```````````````````````````````` example + +Term 1 +: This is a definition item + With a paragraph + > This is a block quote + + - This is a list + - with an item2 + + ```java + Test + + + ``` + + And a last line +: This ia another definition item + +Term2 +Term3 *with some inline* +: This is another definition for term2 +. +
+
Term 1
+

This is a definition item +With a paragraph

+
+

This is a block quote

+
+ +
Test
+
+
+
+

And a last line

+
+
This ia another definition item
+
Term2
+
Term3 with some inline
+
This is another definition for term2
+
+```````````````````````````````` + +A definition term can be followed at most by one blank line. Lazy continuations are supported for definitions: + +```````````````````````````````` example +Term 1 + +: Definition +with lazy continuation. + + Second paragraph of the definition. +. +
+
Term 1
+

Definition +with lazy continuation.

+

Second paragraph of the definition.

+
+
+```````````````````````````````` + +The definition must be indented to 4 characters including the `:`. + +```````````````````````````````` example +Term 1 +: Invalid with less than 3 characters +. +

Term 1 +: Invalid with less than 3 characters

+```````````````````````````````` + +The `:` can be indented up to 3 spaces: + +```````````````````````````````` example +Term 1 + : Valid even if `:` starts at most 3 spaces +. +
+
Term 1
+
Valid even if : starts at most 3 spaces
+
+```````````````````````````````` + +But more than 3 spaces before `:` will trigger an indented code block: + +```````````````````````````````` example +Term 1 + + : Not valid +. +

Term 1

+
: Not valid
+
+```````````````````````````````` + +Definition lists can be nested inside list items + +```````````````````````````````` example +1. First + +2. Second + + Term 1 + : Definition + + Term 2 + : Second Definition +. +
    +
  1. First

  2. +
  3. Second

    +
    +
    Term 1
    +
    Definition
    +
    Term 2
    +
    Second Definition
    +
  4. +
+```````````````````````````````` diff --git a/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs b/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs new file mode 100644 index 0000000..85eb808 --- /dev/null +++ b/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs @@ -0,0 +1,208 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Parsers; +using Markdig.Syntax; + +namespace Markdig.Extensions.DefinitionLists; + +/// +/// The block parser for a . +/// +/// +public class DefinitionListParser : BlockParser +{ + /// + /// Initializes a new instance of the class. + /// + public DefinitionListParser() + { + OpeningCharacters = [':', '~']; + } + + /// + /// Attempts to open a block at the current parser position. + /// + public override BlockState TryOpen(BlockProcessor processor) + { + var paragraphBlock = processor.LastBlock as ParagraphBlock; + if (processor.IsCodeIndent || paragraphBlock is null || paragraphBlock.LastLine - processor.LineIndex > 1) + { + return BlockState.None; + } + + var startPosition = processor.Start; + + var column = processor.ColumnBeforeIndent; + processor.NextChar(); + processor.ParseIndent(); + var delta = processor.Column - column; + + // We expect to have a least + if (delta < 4) + { + // Return back to original position + processor.GoToColumn(column); + return BlockState.None; + } + + if (delta > 4) + { + processor.GoToColumn(column + 4); + } + + var previousParent = paragraphBlock.Parent!; + var currentDefinitionList = GetCurrentDefinitionList(paragraphBlock, previousParent); + + processor.Discard(paragraphBlock); + + // If the paragraph block was not part of the opened blocks, we need to remove it manually from its parent container + if (paragraphBlock.Parent != null) + { + paragraphBlock.Parent.Remove(paragraphBlock); + } + + if (currentDefinitionList is null) + { + currentDefinitionList = new DefinitionList(this) + { + Span = new SourceSpan(paragraphBlock.Span.Start, processor.Line.End), + Column = paragraphBlock.Column, + Line = paragraphBlock.Line, + }; + previousParent.Add(currentDefinitionList); + } + + var definitionItem = new DefinitionItem(this) + { + Line = processor.LineIndex, + Column = column, + Span = new SourceSpan(paragraphBlock.Span.Start, processor.Line.End), + OpeningCharacter = processor.CurrentChar, + }; + + for (int i = 0; i < paragraphBlock.Lines.Count; i++) + { + var line = paragraphBlock.Lines.Lines[i]; + var term = new DefinitionTerm(this) + { + Column = paragraphBlock.Column, + Line = line.Line, + Span = new SourceSpan(paragraphBlock.Span.Start, paragraphBlock.Span.End), + IsOpen = false + }; + term.AppendLine(ref line.Slice, line.Column, line.Line, line.Position, processor.TrackTrivia); + definitionItem.Add(term); + } + currentDefinitionList.Add(definitionItem); + processor.Open(definitionItem); + + // Update the end position + currentDefinitionList.UpdateSpanEnd(processor.Line.End); + + return BlockState.Continue; + } + + private static DefinitionList? GetCurrentDefinitionList(ParagraphBlock paragraphBlock, ContainerBlock previousParent) + { + var index = previousParent.IndexOf(paragraphBlock) - 1; + if (index < 0) return null; + switch (previousParent[index]) + { + case DefinitionList definitionList: + return definitionList; + + case BlankLineBlock: + if (index > 0 && previousParent[index - 1] is DefinitionList definitionList2) + { + previousParent.RemoveAt(index); + return definitionList2; + } + break; + } + return null; + } + + /// + /// Attempts to continue parsing the specified block. + /// + public override BlockState TryContinue(BlockProcessor processor, Block block) + { + var definitionItem = (DefinitionItem)block; + if (processor.IsCodeIndent) + { + processor.GoToCodeIndent(); + return BlockState.Continue; + } + + var list = (DefinitionList)definitionItem.Parent!; + var lastBlankLine = definitionItem.LastChild as BlankLineBlock; + + // Check if we have another definition list + if (Array.IndexOf(OpeningCharacters!, processor.CurrentChar) >= 0) + { + var startPosition = processor.Start; + var column = processor.ColumnBeforeIndent; + processor.NextChar(); + processor.ParseIndent(); + var delta = processor.Column - column; + + // We expect to have a least + if (delta < 4) + { + // Remove the blankline before breaking this definition item + if (lastBlankLine != null) + { + definitionItem.RemoveAt(definitionItem.Count - 1); + } + + list.Span.End = list.LastChild!.Span.End; + return BlockState.None; + } + + if (delta > 4) + { + processor.GoToColumn(column + 4); + } + + processor.Close(definitionItem); + var nextDefinitionItem = new DefinitionItem(this) + { + Span = new SourceSpan(startPosition, processor.Line.End), + Line = processor.LineIndex, + Column = processor.Column, + OpeningCharacter = processor.CurrentChar, + }; + list.Add(nextDefinitionItem); + processor.Open(nextDefinitionItem); + + return BlockState.Continue; + } + + var isBreakable = definitionItem.LastChild?.IsBreakable ?? true; + if (processor.IsBlankLine) + { + if (lastBlankLine is null && isBreakable) + { + definitionItem.Add(new BlankLineBlock()); + } + return isBreakable ? BlockState.ContinueDiscard : BlockState.Continue; + } + + var paragraphBlock = definitionItem.LastChild as ParagraphBlock; + if (lastBlankLine is null && paragraphBlock != null) + { + return BlockState.Continue; + } + + // Remove the blankline before breaking this definition item + if (lastBlankLine != null) + { + definitionItem.RemoveAt(definitionItem.Count - 1); + } + + list.Span.End = list.LastChild!.Span.End; + return BlockState.Break; + } +}