diff --git a/src/MarkdownDeep/Block.cs b/src/MarkdownDeep/Block.cs
index b09b93d..7b500ff 100644
--- a/src/MarkdownDeep/Block.cs
+++ b/src/MarkdownDeep/Block.cs
@@ -59,6 +59,8 @@ namespace MarkdownDeep
// DocNet extensions
font_awesome, // Data is icon name
alert, // Data is the alert type specified.
+ tab, // Data is the tab header text.
+ tabs, // children are the tabs.
}
class Block
@@ -385,7 +387,12 @@ namespace MarkdownDeep
RenderAlert(m, b);
}
break;
-
+ case BlockType.tabs:
+ if(m.DocNetMode)
+ {
+ RenderTabs(m, b);
+ }
+ break;
// End DocNet Extensions
default:
b.Append("<" + BlockType.ToString() + ">");
@@ -395,7 +402,6 @@ namespace MarkdownDeep
}
}
-
internal void RenderPlain(Markdown m, StringBuilder b)
{
switch (BlockType)
@@ -513,13 +519,60 @@ namespace MarkdownDeep
b.Append(alertType);
b.Append("\">");
+ b.Append("\"> ");
b.Append(title);
b.Append("");
RenderChildren(m, b);
b.Append("");
}
+
+ private void RenderTabs(Markdown m, StringBuilder b)
+ {
+ var tabId = m.GetNewTabId();
+ var headerSB = new StringBuilder();
+ var contentSB = new StringBuilder();
+ var tabCounter = 0;
+ var checkedAttribute = " checked";
+ foreach(var tabBlock in this.Children)
+ {
+ if(tabBlock.BlockType != BlockType.tab)
+ {
+ return;
+ }
+ var tabHeaderText = tabBlock.Data as string ?? "Tab";
+ // header
+ string tabIdSuffix = tabCounter + "_" + tabId;
+ headerSB.Append("");
+
+ // content
+ var tabContentSB = new StringBuilder();
+ tabBlock.RenderChildren(m, tabContentSB);
+ contentSB.Append("
");
+ contentSB.Append(tabContentSB.ToString());
+ contentSB.Append("
");
+
+ // done
+ checkedAttribute = string.Empty;
+ tabCounter++;
+ }
+ b.Append("");
+ b.Append(headerSB.ToString());
+ b.Append(contentSB.ToString());
+ b.Append("
");
+ }
+
+
#region Properties
public int ContentEnd
{
diff --git a/src/MarkdownDeep/BlockProcessor.cs b/src/MarkdownDeep/BlockProcessor.cs
index 2396382..6fb3996 100644
--- a/src/MarkdownDeep/BlockProcessor.cs
+++ b/src/MarkdownDeep/BlockProcessor.cs
@@ -1268,10 +1268,15 @@ namespace MarkdownDeep
{
return HandleFontAwesomeExtension(b);
}
+ // first match @tabs, and then @tab, as both are handled by this processor.
if(DoesMatch("@tabs"))
{
return HandleTabsExtension(b);
}
+ if(DoesMatch("@tab"))
+ {
+ return HandleTabForTabsExtension(b);
+ }
if(DoesMatch("@alert"))
{
return HandleAlertExtension(b);
@@ -1280,6 +1285,7 @@ namespace MarkdownDeep
}
+
///
/// Handles the alert extension:
/// @alert type
@@ -1327,21 +1333,7 @@ namespace MarkdownDeep
}
// Remove the trailing line end
- if(Input[endContent - 1] == '\r' && Input[endContent - 2] == '\n')
- {
- endContent -= 2;
- }
- else
- {
- if(Input[endContent - 1] == '\n' && Input[endContent - 2] == '\r')
- {
- endContent -= 2;
- }
- else
- {
- endContent--;
- }
- }
+ endContent = UnskipCRLFBeforePos(endContent);
b.BlockType = BlockType.alert;
b.Data = alertType;
// scan the content, as it can contain markdown statements.
@@ -1350,10 +1342,101 @@ namespace MarkdownDeep
return true;
}
+
private bool HandleTabsExtension(Block b)
{
-#warning IMPLEMENT
- return false;
+ // skip '@tabs'
+ if(!SkipString("@tabs"))
+ {
+ return false;
+ }
+ // ignore what's specified behind @tabs
+ SkipToNextLine();
+ int startContent = this.Position;
+ // find @end.
+ if(!Find("@endtabs"))
+ {
+ return false;
+ }
+ // Character before must be a eol char
+ if(!IsLineEnd(CharAtOffset(-1)))
+ {
+ return false;
+ }
+ int endContent = Position;
+ // skip @end
+ SkipString("@endtabs");
+ SkipLinespace();
+ if(!Eol)
+ {
+ return false;
+ }
+ // Remove the trailing line end
+ endContent = UnskipCRLFBeforePos(endContent);
+ b.BlockType = BlockType.tabs;
+ // scan the content, as it can contain markdown statements.
+ var contentProcessor = new BlockProcessor(m_markdown, m_markdown.MarkdownInHtml);
+ var scanLines = contentProcessor.ScanLines(this.Input, startContent, endContent - startContent);
+ // check whether the content is solely tab blocks. If not, we ignore this tabs specification.
+ if(scanLines.Any(x=>x.BlockType != BlockType.tab))
+ {
+ return false;
+ }
+ b.Children = scanLines;
+ return true;
+ }
+
+
+ ///
+ /// Handles the tab for tabs extension. This is a docnet extension and it handles:
+ /// @tab tab head text
+ /// tab content
+ /// @end
+ ///
+ /// The current block.
+ ///
+ private bool HandleTabForTabsExtension(Block b)
+ {
+ // skip '@tab'
+ if(!SkipString("@tab"))
+ {
+ return false;
+ }
+ SkipLinespace();
+ var tabHeaderTextStart = this.Position;
+ // skip to eol, then grab the content between positions.
+ SkipToEol();
+ var tabHeaderText = this.Input.Substring(tabHeaderTextStart, this.Position - tabHeaderTextStart);
+ SkipToNextLine();
+ int startContent = this.Position;
+
+ // find @end.
+ if(!Find("@end"))
+ {
+ return false;
+ }
+ // Character before must be a eol char
+ if(!IsLineEnd(CharAtOffset(-1)))
+ {
+ return false;
+ }
+ int endContent = Position;
+ // skip @end
+ SkipString("@end");
+ SkipLinespace();
+ if(!Eol)
+ {
+ return false;
+ }
+
+ // Remove the trailing line end
+ endContent = UnskipCRLFBeforePos(endContent);
+ b.BlockType = BlockType.tab;
+ b.Data = tabHeaderText;
+ // scan the content, as it can contain markdown statements.
+ var contentProcessor = new BlockProcessor(m_markdown, m_markdown.MarkdownInHtml);
+ b.Children = contentProcessor.ScanLines(Input, startContent, endContent - startContent);
+ return true;
}
diff --git a/src/MarkdownDeep/MardownDeep.cs b/src/MarkdownDeep/MardownDeep.cs
index 6b1c585..abea7c9 100644
--- a/src/MarkdownDeep/MardownDeep.cs
+++ b/src/MarkdownDeep/MardownDeep.cs
@@ -42,6 +42,7 @@ namespace MarkdownDeep
private Dictionary m_UsedHeaderIDs;
private Dictionary m_AbbreviationMap;
private List m_AbbreviationList;
+ private int _tabIdCounter;
#endregion
// Constructor
@@ -57,6 +58,7 @@ namespace MarkdownDeep
m_UsedFootnotes = new List();
m_UsedHeaderIDs = new Dictionary();
this.CreatedH2IdCollector = new List>();
+ _tabIdCounter = 0;
}
internal List ProcessBlocks(string str)
@@ -551,6 +553,16 @@ namespace MarkdownDeep
return sb.ToString();
}
+ ///
+ /// Gets the new tab identifier, which is a unique number within the markdown parsing pass.
+ ///
+ ///
+ internal int GetNewTabId()
+ {
+ _tabIdCounter++;
+ return _tabIdCounter;
+ }
+
// Add a link definition
internal void AddLinkDefinition(LinkDefinition link)
{
@@ -1019,7 +1031,6 @@ namespace MarkdownDeep
get;
set;
}
-
#endregion
}
diff --git a/src/MarkdownDeep/StringScanner.cs b/src/MarkdownDeep/StringScanner.cs
index ceaebee..0548cc3 100644
--- a/src/MarkdownDeep/StringScanner.cs
+++ b/src/MarkdownDeep/StringScanner.cs
@@ -537,5 +537,33 @@ namespace MarkdownDeep
int pos;
int end;
int mark;
+
+
+ ///
+ /// Unskips the CRLF before position. This simply means it returns the new position calculated from the specified position if before the specified
+ /// position a CRLF is present.
+ ///
+ /// The position.
+ /// position minus 0, 1 or 2 depending on whether a CRLF is present right before position: if so, it returns position - the length of the CRLF
+ /// which can be 1 or 2 depending on the fact whether it's \r or \n or both.
+ protected int UnskipCRLFBeforePos(int position)
+ {
+ if(this.Input[position - 1] == '\r' && this.Input[position - 2] == '\n')
+ {
+ position -= 2;
+ }
+ else
+ {
+ if(this.Input[position - 1] == '\n' && this.Input[position - 2] == '\r')
+ {
+ position -= 2;
+ }
+ else
+ {
+ position--;
+ }
+ }
+ return position;
+ }
}
}
diff --git a/src/MarkdownDeepTests/MarkdownDeepTests.csproj b/src/MarkdownDeepTests/MarkdownDeepTests.csproj
index 1babcd2..bce03b3 100644
--- a/src/MarkdownDeepTests/MarkdownDeepTests.csproj
+++ b/src/MarkdownDeepTests/MarkdownDeepTests.csproj
@@ -467,6 +467,12 @@
+
+
+
+
+
+