Skip to content

Commit 72932dd

Browse files
committed
Implement YAML block additional feature
This implements support for parsing YAML blocks and front-matter following the Pandoc rules: 1. Delimited by three hyphens (---) at the top 2. Delimited by three hyphens (---) or dots (...) at the bottom 3. May occur anywhere in the document, but if not at the beginning must be preceded by a blank line. BlockTag.YamlBlock is defined, and FencedCodeData is used to store the closing fence character (either `-` or `.`) and to indicate whether the closing fence has been seen in the same manner as BlockTag.FencedCode. YAML support may be enabled via two-flavors: 1. CommonMarkAdditionalFeatures.YamlBlocks: this allows for any number of YAML blocks anywhere in the document. 2. CommonMarkAdditionalFeatures.YamlFrontMatterOnly: allows for exactly one YAML block, defined on the first line of the document. The HTML formatters treat BlockTag.YamlBlock blocks the same way as BlockTag.FencedCode blocks, except that instead of writing the info string, a `class="language-yaml"` attribute will be written.
1 parent 99360a0 commit 72932dd

9 files changed

+194
-5
lines changed

CommonMark.Tests/CommonMark.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
<DesignTime>True</DesignTime>
7676
<DependentUpon>Specs.tt</DependentUpon>
7777
</Compile>
78+
<Compile Include="YamlBlockTests.cs" />
7879
</ItemGroup>
7980
<ItemGroup>
8081
<None Include="Specification\Specs.tt">

CommonMark.Tests/YamlBlockTests.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using Microsoft.VisualStudio.TestTools.UnitTesting;
2+
3+
using CommonMark.Syntax;
4+
5+
namespace CommonMark.Tests
6+
{
7+
[TestClass]
8+
public class YamlBlockTests
9+
{
10+
private const string Category = "Container blocks - YAML";
11+
12+
private static CommonMarkSettings GetSettings(bool frontMatterOnly = false)
13+
{
14+
var settings = CommonMarkSettings.Default.Clone();
15+
settings.AdditionalFeatures = frontMatterOnly
16+
? CommonMarkAdditionalFeatures.YamlFrontMatterOnly
17+
: CommonMarkAdditionalFeatures.YamlBlocks;
18+
return settings;
19+
}
20+
21+
[TestMethod]
22+
[TestCategory(Category)]
23+
public void YamlDisabled()
24+
{
25+
Helpers.ExecuteTest(
26+
"---\nparagraph\n...",
27+
"<hr />\n<p>paragraph\n...</p>");
28+
}
29+
30+
[TestMethod]
31+
[TestCategory(Category)]
32+
public void YamlEmpty()
33+
{
34+
Helpers.ExecuteTest(
35+
"---\n\n...",
36+
"<pre><code class=\"language-yaml\">\n</code></pre>",
37+
GetSettings());
38+
}
39+
40+
[TestMethod]
41+
[TestCategory(Category)]
42+
public void YamlFrontMatterOnly()
43+
{
44+
Helpers.ExecuteTest(
45+
"---\nfrontmatter\n...\n\nparagraph\n\n---\nline 1\nline 2\n...\n",
46+
"<pre><code class=\"language-yaml\">frontmatter\n</code></pre>\n" +
47+
"<p>paragraph</p>\n" +
48+
"<hr />\n" +
49+
"<p>line 1\nline 2\n...</p>",
50+
GetSettings(true));
51+
}
52+
53+
[TestMethod]
54+
[TestCategory(Category)]
55+
public void YamlMultipleBlocks()
56+
{
57+
Helpers.ExecuteTest(
58+
"---\nfrontmatter\n...\n\nparagraph\n\n---\nline 1\nline 2\n...\n",
59+
"<pre><code class=\"language-yaml\">frontmatter\n</code></pre>\n" +
60+
"<p>paragraph</p>\n" +
61+
"<pre><code class=\"language-yaml\">line 1\nline 2\n</code></pre>",
62+
GetSettings());
63+
}
64+
65+
[TestMethod]
66+
[TestCategory(Category)]
67+
public void YamlAndThematicBreak()
68+
{
69+
Helpers.ExecuteTest(
70+
"----\n\nnot yaml\n---\nalso not yaml\n\n---\nbut this\nis\nyaml\n...\npara",
71+
"<hr />\n" +
72+
"<h2>not yaml</h2>\n" +
73+
"<p>also not yaml</p>\n" +
74+
"<pre><code class=\"language-yaml\">but this\nis\nyaml\n</code></pre>\n" +
75+
"<p>para</p>",
76+
GetSettings());
77+
}
78+
79+
[TestMethod]
80+
[TestCategory(Category)]
81+
public void YamlClosingFenceDash()
82+
{
83+
AssertYamlClosingFenceAndAst("---");
84+
}
85+
86+
[TestMethod]
87+
[TestCategory(Category)]
88+
public void YamlClosingFenceDot()
89+
{
90+
AssertYamlClosingFenceAndAst("...");
91+
}
92+
93+
private static void AssertYamlClosingFenceAndAst(string fence)
94+
{
95+
var markdown = "---\nyaml\n" + fence;
96+
Helpers.ExecuteTest(
97+
markdown,
98+
"<pre><code class=\"language-yaml\">yaml\n</code></pre>",
99+
GetSettings());
100+
101+
var doc = CommonMarkConverter.Parse(markdown, GetSettings());
102+
103+
Assert.IsNotNull(doc.FirstChild);
104+
Assert.AreEqual(BlockTag.YamlBlock, doc.FirstChild.Tag);
105+
Assert.IsNotNull(doc.FirstChild.FencedCodeData);
106+
Assert.AreEqual(0, doc.FirstChild.FencedCodeData.FenceOffset);
107+
Assert.AreEqual(-1, doc.FirstChild.FencedCodeData.FenceLength);
108+
Assert.AreEqual(fence[0], doc.FirstChild.FencedCodeData.FenceChar);
109+
Assert.IsNull(doc.FirstChild.FencedCodeData.Info);
110+
}
111+
}
112+
}

CommonMark/CommonMarkAdditionalFeatures.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ public enum CommonMarkAdditionalFeatures
2525
/// </summary>
2626
PlaceholderBracket = 2,
2727

28+
/// <summary>
29+
/// Allow YAML blocks (delimited by a line starting with exactly <c>---</c> and a line ending
30+
/// with exactly <c>---</c> or <c>...</c>. YAML blocks will take precedence over a <c>---</c>
31+
/// that might otherwise yield a thematic break.
32+
/// </summary>
33+
YamlBlocks = 4,
34+
35+
/// <summary>
36+
/// Like <see cref="YamlBlocks"/> but will yield a maximum of one block, and only if the first
37+
/// line is exactly <c>---</c>.
38+
/// </summary>
39+
YamlFrontMatterOnly = 8,
40+
2841
/// <summary>
2942
/// All additional features are enabled.
3043
/// </summary>

CommonMark/CommonMarkConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public static Syntax.Block ProcessStage1(TextReader source, CommonMarkSettings s
111111
reader.ReadLine(line);
112112
while (line.Line != null)
113113
{
114-
BlockMethods.IncorporateLine(line, ref cur);
114+
BlockMethods.IncorporateLine(line, ref cur, settings);
115115
reader.ReadLine(line);
116116
}
117117
}

CommonMark/Formatters/HtmlFormatter.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ protected virtual void WriteBlock(Block block, bool isOpening, bool isClosing, o
219219

220220
case BlockTag.IndentedCode:
221221
case BlockTag.FencedCode:
222+
case BlockTag.YamlBlock:
222223

223224
ignoreChildNodes = true;
224225

@@ -237,6 +238,11 @@ protected virtual void WriteBlock(Block block, bool isOpening, bool isClosing, o
237238
WriteEncodedHtml(new StringPart(info, 0, x));
238239
Write('\"');
239240
}
241+
else if (block.Tag == BlockTag.YamlBlock)
242+
{
243+
Write(" class=\"language-yaml\"");
244+
}
245+
240246
Write('>');
241247
WriteEncodedHtml(block.StringContent);
242248
WriteLine("</code></pre>");

CommonMark/Formatters/HtmlFormatterSlim.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ private static void BlocksToHtmlInner(HtmlTextWriter writer, Block block, Common
318318

319319
case BlockTag.IndentedCode:
320320
case BlockTag.FencedCode:
321+
case BlockTag.YamlBlock:
321322
writer.EnsureLine();
322323
writer.WriteConstant("<pre><code");
323324
if (trackPositions) PrintPosition(writer, block);
@@ -333,6 +334,11 @@ private static void BlocksToHtmlInner(HtmlTextWriter writer, Block block, Common
333334
EscapeHtml(new StringPart(info, 0, x), writer);
334335
writer.Write('\"');
335336
}
337+
else if (block.Tag == BlockTag.YamlBlock)
338+
{
339+
writer.WriteConstant(" class=\"language-yaml\"");
340+
}
341+
336342
writer.Write('>');
337343
EscapeHtml(block.StringContent, writer);
338344
writer.WriteLineConstant("</code></pre>");

CommonMark/Formatters/Printer.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ public static void PrintBlocks(TextWriter writer, Block block, CommonMarkSetting
162162
format_str(block.StringContent.ToString(buffer), buffer));
163163
break;
164164

165+
case BlockTag.YamlBlock:
166+
writer.Write("yaml_block");
167+
PrintPosition(trackPositions, writer, block);
168+
writer.Write(" closing_fence_char={0} {1}",
169+
block.FencedCodeData.FenceChar,
170+
format_str(block.StringContent.ToString(buffer), buffer));
171+
break;
172+
165173
case BlockTag.HtmlBlock:
166174
writer.Write("html_block");
167175
PrintPosition(trackPositions, writer, block);

CommonMark/Parser/BlockMethods.cs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ private static bool AcceptsLines(BlockTag block_type)
2929
return (block_type == BlockTag.Paragraph ||
3030
block_type == BlockTag.AtxHeading ||
3131
block_type == BlockTag.IndentedCode ||
32-
block_type == BlockTag.FencedCode);
32+
block_type == BlockTag.FencedCode ||
33+
block_type == BlockTag.YamlBlock);
3334
}
3435

3536
private static void AddLine(Block block, LineInfo lineInfo, string ln, int offset, int remainingSpaces, int length = -1)
@@ -137,9 +138,12 @@ public static void Finalize(Block b, LineInfo line)
137138
break;
138139

139140
case BlockTag.FencedCode:
141+
case BlockTag.YamlBlock:
140142
// first line of contents becomes info
141143
var firstlinelen = b.StringContent.IndexOf('\n') + 1;
142-
b.FencedCodeData.Info = InlineMethods.Unescape(b.StringContent.TakeFromStart(firstlinelen, true).Trim());
144+
var firstline = b.StringContent.TakeFromStart(firstlinelen, true);
145+
if (b.Tag == BlockTag.FencedCode)
146+
b.FencedCodeData.Info = InlineMethods.Unescape(firstline.Trim());
143147
break;
144148

145149
case BlockTag.List: // determine tight/loose status
@@ -464,7 +468,7 @@ private static void AdvanceOffset(string line, int count, bool columns, ref int
464468
// Process one line at a time, modifying a block.
465469
// Returns 0 if successful. curptr is changed to point to
466470
// the currently open block.
467-
public static void IncorporateLine(LineInfo line, ref Block curptr)
471+
public static void IncorporateLine(LineInfo line, ref Block curptr, CommonMarkSettings settings)
468472
{
469473
var ln = line.Line;
470474

@@ -571,6 +575,7 @@ public static void IncorporateLine(LineInfo line, ref Block curptr)
571575
}
572576

573577
case BlockTag.FencedCode:
578+
case BlockTag.YamlBlock:
574579
{
575580
// -1 means we've seen closer
576581
if (container.FencedCodeData.FenceLength == -1)
@@ -632,6 +637,7 @@ public static void IncorporateLine(LineInfo line, ref Block curptr)
632637
// unless last matched container is code block, try new container starts:
633638
while (container.Tag != BlockTag.FencedCode &&
634639
container.Tag != BlockTag.IndentedCode &&
640+
container.Tag != BlockTag.YamlBlock &&
635641
container.Tag != BlockTag.HtmlBlock)
636642
{
637643

@@ -670,6 +676,20 @@ public static void IncorporateLine(LineInfo line, ref Block curptr)
670676

671677
AdvanceOffset(ln, first_nonspace + matched - offset, false, ref offset, ref column, ref remainingSpaces);
672678

679+
}
680+
else if (!indented &&
681+
((container.IsLastLineBlank && (settings.AdditionalFeatures & CommonMarkAdditionalFeatures.YamlBlocks) != 0)
682+
|| (line.LineNumber == 1 && (settings.AdditionalFeatures & (CommonMarkAdditionalFeatures.YamlFrontMatterOnly | CommonMarkAdditionalFeatures.YamlBlocks)) != 0))
683+
&& ln == "---\n")
684+
{
685+
686+
container = CreateChildBlock(container, line, BlockTag.YamlBlock, first_nonspace);
687+
container.FencedCodeData = new FencedCodeData();
688+
container.FencedCodeData.FenceChar = '-';
689+
container.FencedCodeData.FenceLength = 3;
690+
691+
AdvanceOffset(ln, 3, false, ref offset, ref column, ref remainingSpaces);
692+
673693
}
674694
else if (!indented && curChar == '<' &&
675695
(0 != (matched = (int)Scanner.scan_html_block_start(ln, first_nonspace, ln.Length))
@@ -795,6 +815,7 @@ public static void IncorporateLine(LineInfo line, ref Block curptr)
795815
container.Tag != BlockTag.BlockQuote &&
796816
container.Tag != BlockTag.SetextHeading &&
797817
container.Tag != BlockTag.FencedCode &&
818+
container.Tag != BlockTag.YamlBlock &&
798819
!(container.Tag == BlockTag.ListItem &&
799820
container.FirstChild == null &&
800821
container.SourcePosition >= line.LineOffset));
@@ -851,6 +872,20 @@ public static void IncorporateLine(LineInfo line, ref Block curptr)
851872
AddLine(container, line, ln, offset, remainingSpaces);
852873
}
853874

875+
}
876+
else if (container.Tag == BlockTag.YamlBlock)
877+
{
878+
879+
if ((curChar == '-' && ln == "---\n") || (curChar == '.' && ln == "...\n"))
880+
{
881+
container.FencedCodeData.FenceLength = -1;
882+
container.FencedCodeData.FenceChar = ln[0];
883+
}
884+
else
885+
{
886+
AddLine(container, line, ln, offset, remainingSpaces);
887+
}
888+
854889
}
855890
else if (container.Tag == BlockTag.HtmlBlock)
856891
{

CommonMark/Syntax/BlockTag.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ public enum BlockTag : byte
8383
/// <summary>
8484
/// A text block that contains only link reference definitions.
8585
/// </summary>
86-
ReferenceDefinition
86+
ReferenceDefinition,
87+
88+
/// <summary>
89+
/// A YAML metadta block (for example, <c>---\nyaml: metadata\n...</c>).
90+
/// Only present if <see cref="CommonMarkAdditionalFeatures.YamlBlocks"/> or
91+
/// <see cref="CommonMarkAdditionalFeatures.YamlFrontMatterOnly"/> are enabled
92+
/// The block is structured like a <see cref="FencedCode"/> block.
93+
/// </summary>
94+
YamlBlock
8795
}
8896
}

0 commit comments

Comments
 (0)