Compare commits

...

62 Commits

Author SHA1 Message Date
Alexandre Mutel
62907f9314 Example API doc generation 2022-06-29 07:00:28 +02:00
Alexandre Mutel
5f80d86265 Merge pull request #642 from mnaoumov/issue-579
More accurate check for YAML renderers
2022-06-15 07:52:51 +02:00
Alexandre Mutel
b85cc0daf5 Merge pull request #644 from PaulVrugt/master
added support for ToPlainText for pipetables extension
2022-06-15 07:52:06 +02:00
Paul Vrugt
76073e81c0 added support for ToPlainText for pipetables extension 2022-06-08 19:47:48 +02:00
Michael Naumov
5b6621d729 Add tests to cover all possible cases of adding Yaml renderers 2022-06-07 15:06:14 -06:00
Michael Naumov
9723eda455 More accurate check for YAML renderers 2022-06-07 14:02:40 -06:00
Alexandre Mutel
7228ad5072 Merge pull request #638 from mnaoumov/issue-579
Add YamlFrontMatterRoundtripRenderer
2022-06-07 21:40:58 +02:00
Alexandre Mutel
96f55d0aa6 Merge pull request #637 from MihaZupan/perf-parse-overhead
Reduce the overhead of Parse calls
2022-06-07 21:38:08 +02:00
Alexandre Mutel
2d69ac4499 Merge pull request #624 from mattj23/docs
Additional parser documentation
2022-06-07 21:37:17 +02:00
Michael Naumov
5e91b9b763 Add YamlFrontMatterRoundtripRenderer 2022-05-17 16:46:24 -06:00
Miha Zupan
e6255de62b Avoid locking overhead in ObjectCache 2022-05-14 19:04:51 +02:00
Miha Zupan
c7b8772669 Avoid SelfPipeline search overhead 2022-05-14 19:03:31 +02:00
Alexandre Mutel
89a10ee76b Remove old benchmark graphs 2022-04-23 17:37:06 +02:00
Alexandre Mutel
7676079b4e Update doc readme with new benchmarks 2022-04-23 17:36:17 +02:00
Alexandre Mutel
495abab743 Update benchmark code and dependencies 2022-04-23 16:51:16 +02:00
Alexandre Mutel
210b39e8fb Improve RenderBase optimization with Type.GetTypeHandle (#632) 2022-04-23 16:21:09 +02:00
Alexandre Mutel
f09d030fd3 Cleanup code after #632 2022-04-23 14:05:26 +02:00
Alexandre Mutel
f2ca6be7a6 Fix name for SpecFileGen 2022-04-23 07:59:18 +02:00
Alexandre Mutel
94e07d11ce Revert optimization for RendererBase to use a plain type for the dictionary key (#632) 2022-04-23 07:46:26 +02:00
Alexandre Mutel
263041e899 Merge pull request #632 from MihaZupan/wasm-mt-ptr
Don't try to use the MT pointer on unsupported platforms
2022-04-23 07:38:16 +02:00
Miha Zupan
e8f9274b64 Don't try to use the MT pointer on unsupported platforms 2022-04-22 20:36:52 +02:00
Alexandre Mutel
b32e71aaeb Merge pull request #630 from MihaZupan/containerblock-copyto-628
Fix typos in ContainerBlock
2022-04-22 07:33:56 +02:00
Miha Zupan
f991b2123b Fix typos in ContainerBlock 2022-04-21 20:48:13 +02:00
Alexandre Mutel
a4a1a177bc Update doc and fix name for CommonMark specs 2022-04-21 07:40:17 +02:00
Alexandre Mutel
59d59694f4 Merge pull request #627 from xoofx/update-commonmark-spec
Update commonmark spec 0.30
2022-04-21 07:30:44 +02:00
Alexandre Mutel
caf3c722e1 Fix unicode case fold for spec 0.30 2022-04-21 07:26:02 +02:00
Alexandre Mutel
47c64d8815 Fix support for textarea with new CommonMark 0.30 specs 2022-04-20 19:04:28 +02:00
Alexandre Mutel
2502fab340 Update CommonMark specs to 0.30 2022-04-20 19:03:47 +02:00
Alexandre Mutel
17b5500b03 Add test related to #625 2022-04-20 18:28:58 +02:00
Alexandre Mutel
b754aef6b0 Update dependencies 2022-04-20 18:28:32 +02:00
Alexandre Mutel
04843a08d2 Merge branch 'fix-escape-line-break' 2022-04-20 18:10:11 +02:00
Alexandre Mutel
fcc73691b6 Fixes escape line break (#620) 2022-04-20 18:09:17 +02:00
Alexandre Mutel
cb8dc99d96 Add test for line break
test
2022-04-20 18:08:28 +02:00
Alexandre Mutel
6f45ac0885 Merge pull request #623 from MihaZupan/globalization-ltr-fix
Fix GlobalizationExtension RTL detection
2022-04-20 15:56:30 +02:00
Alexandre Mutel
891e2fca78 Merge pull request #622 from MihaZupan/simd-character-map
Vectorize CharacterMap.IndexOfOpeningCharacter
2022-04-20 15:56:05 +02:00
Alexandre Mutel
925d4f9227 Merge pull request #621 from MihaZupan/perf-april
April perf improvements
2022-04-20 15:54:48 +02:00
Matt Jarvis
f0c200fc28 Updates to parsing documentation, inlines 2022-04-15 18:16:07 -04:00
Matt Jarvis
70184179b7 Merge remote-tracking branch 'upstream/master' into docs 2022-04-15 15:45:16 -04:00
Miha Zupan
d69b989810 Fix GlobalizationExtension RTL detection 2022-04-15 20:09:50 +02:00
Miha Zupan
da756f4efe Vectorize CharacterMap.IndexOfOpeningCharacter 2022-04-15 19:43:04 +02:00
Miha Zupan
e192831db0 Add tests for LazySubstring 2022-04-15 16:33:36 +02:00
Miha Zupan
be3c93a9b0 Remove trailing space 2022-04-15 16:21:39 +02:00
Miha Zupan
6466f01a80 Add fast-path for simple GetSourcePosition 2022-04-10 18:05:00 +02:00
Alexandre Mutel
cb26f30f7b Merge pull request #607 from mattj23/docs
Start of documentation on parser
2022-04-10 10:09:41 +02:00
Miha Zupan
147c3f059a Improve the fast-path in AutoLink parser 2022-04-08 18:36:41 +02:00
Miha Zupan
3201699053 Avoid allocating CodeInline.Content substrings 2022-04-08 17:22:38 +02:00
Miha Zupan
e86d1ffce5 Avoid OrderedStart allocations in NumberedListItemParser 2022-04-08 16:39:21 +02:00
Miha Zupan
48c979dc74 Use FastStringWriter as the dummy writer
Avoids allocation when setting Writer.NewLine
2022-04-08 00:21:27 +02:00
Miha Zupan
3ae0c8b369 Revert 6eacf8a 2022-04-08 00:09:22 +02:00
Miha Zupan
ee732e5a42 Add ToHtml helper accepting a TextWriter 2022-04-08 00:02:24 +02:00
Miha Zupan
76e25833ad Use method table pointer instead of TypeHandle 2022-04-03 19:31:33 +02:00
Miha Zupan
53dff53260 Make LinkInline SourceSpan fields non-nullable 2022-04-03 16:44:08 +02:00
Miha Zupan
b2eeaf7185 Store trivia next to DataEntries 2022-04-03 16:19:02 +02:00
Miha Zupan
47a22bc5e8 Reduce the size of LiteralInline and EmphasisDelimiterInline 2022-04-03 15:50:07 +02:00
Miha Zupan
89e4c29f9f Inline simple property getters 2022-04-03 15:08:46 +02:00
Miha Zupan
a946c6d0b4 Use local over list access in GetSourcePosition 2022-04-03 15:02:44 +02:00
Miha Zupan
e2770d8c11 Reduce covariance check overhead 2022-04-03 14:28:02 +02:00
Miha Zupan
6eacf8a170 Reduce casts when rendering 2022-04-03 13:34:38 +02:00
Miha Zupan
e11a2630b8 Reduce the size of Inline and casting overhead 2022-04-03 13:34:06 +02:00
Matt Jarvis
4169e538af Merge remote-tracking branch 'upstream/master' into docs 2022-04-02 22:22:11 -04:00
Matt Jarvis
25db6cb414 updated with initial comments 2022-03-15 21:01:45 -04:00
Matt Jarvis
d42b297128 initial documentation on parser 2022-03-13 12:54:41 -04:00
78 changed files with 6001 additions and 3569 deletions

3
.gitignore vendored
View File

@@ -242,3 +242,6 @@ _Pvt_Extensions
# Remove artifacts produced by dotnet-releaser
artifacts-dotnet-releaser/
# Remove .lunet temp folder
.lunet/build

134
doc/parsing-ast.md Normal file
View File

@@ -0,0 +1,134 @@
# The Abstract Syntax Tree
If successful, the `Markdown.Parse(...)` method returns the abstract syntax tree (AST) of the source text.
This will be an object of the `MarkdownDocument` type, which is in turn derived from a more general block container and is part of a larger taxonomy of classes which represent different semantic constructs of a markdown syntax tree.
This document will discuss the different types of elements within the Markdig representation of the AST.
## Structure of the AST
Within Markdig, there are two general types of node in the markdown syntax tree: `Block`, and `Inline`. Block nodes may contain inline nodes, but the reverse is not true. Blocks may contain other blocks, and inlines may contain other inlines.
The root of the AST is the `MarkdownDocument` which is itself derived from a container block but also contains information on the line count and starting positions within the document. Nodes in the AST have links both to parent and children, allowing the edges in the tree to be traversed efficiently in either direction.
Different semantic constructs are represented by types derived from the `Block` and `Inline` types, which are both `abstract` themselves. These elements are produced by `BlockParser` and `InlineParser` derived types, respectively, and so new constructs can be added with the implementation of a new block or inline parser and a new block or inline type, as well as an extension to register it in the pipeline. For more information on extending Markdig this way refer to the [Extensions/Parsers](parsing-extensions.md) document.
The AST is assembled by the static method `Markdown.Parse(...)` using the collections of block and inline parsers contained in the `MarkdownPipeline`. For more detailed information refer to the [Markdig Parsing Overview](parsing-overview.md) document.
### Quick Examples: Descendants API
The easiest way to traverse the abstract syntax tree is with a group of extension methods that have the name `Descendants`. Several different overloads exist to allow it to search for both `Block` and `Inline` elements, starting from any node in the tree.
The `Descendants` methods return `IEnumerable<MarkdownObject>` or `IEnumerable<T>` as their results. Internally they are using `yield return` to perform edge traversals lazily.
#### Depth-First Like Traversal of All Elements
```csharp
MarkdownDocument result = Markdown.Parse(sourceText, pipeline);
// Iterate through all MarkdownObjects in a depth-first order
foreach (var item in result.Descendants())
{
Console.WriteLine(item.GetType());
// You can use pattern matching to isolate elements of certain type,
// otherwise you can use the filtering mechanism demonstrated in the
// next section
if (item is ListItemBlock listItem)
{
// ...
}
}
```
#### Filtering of Specific Child Types
Filtering can be performed using the `Descendants<T>()` method, in which T is required to be derived from `MarkdownObject`.
```csharp
MarkdownDocument result = Markdown.Parse(sourceText, pipeline);
// Iterate through all ListItem blocks
foreach (var item in result.Descendants<ListItemBlock>())
{
// ...
}
// Iterate through all image links
foreach (var item in result.Descendants<LinkInline>().Where(x => x.IsImage))
{
// ...
}
```
#### Combined Hierarchies
The `Descendants` method can be used on any `MarkdownObject`, not just the root node, so complex hierarchies can be queried.
```csharp
MarkdownDocument result = Markdown.Parse(sourceText, pipeline);
// Find all Emphasis inlines which descend from a ListItem block
var items = document.Descendants<ListItemBlock>()
.Select(block => block.Descendants<EmphasisInline>());
// Find all Emphasis inlines whose direct parent block is a ListItem
var other = document.Descendants<EmphasisInline>()
.Where(inline => inline.ParentBlock is ListItemBlock);
```
## Block Elements
Block elements all derive from `Block` and may be one of two types:
1. `ContainerBlock`, which is a block which holds other blocks (`MarkdownDocument` is itself derived from this)
2. `LeafBlock`, which is a block that has no child blocks, but may contain inlines
Block elements in markdown refer to things like paragraphs, headings, lists, code, etc. Most blocks may contain inlines, with the exception of things like code blocks.
### Properties of Blocks
The following are properties of `Block` objects which warrant elaboration. For a full list of properties see the generated API documentation (coming soon).
#### Block Parent
All blocks have a reference to a parent (`Parent`) of type `ContainerBlock?`, which allows for efficient traversal up the abstract syntax tree. The parent will be `null` in the case of the root node (the `MarkdownDocument`).
#### Parser
All blocks have a reference to a parser (`Parser`) of type `BlockParser?` which refers to the instance of the parser which created this block.
#### IsOpen Flag
Blocks have an `IsOpen` boolean flag which is set true while they're being parsed and then closed when parsing is complete.
Blocks are created by `BlockParser` objects which are managed by an instance of a `BlockProcessor` object. During the parsing algorithm the `BlockProcessor` maintains a list of all currently open `Block` objects as it steps through the source line by line. The `IsOpen` flag indicates to the `BlockProcessor` that the block should remain open as the next line begins. If the `IsOpen` flag is not directly set by the `BlockParser` on each line, the `BlockProcessor` will consider the `Block` fully parsed and will no longer call its `BlockParser` on it.
#### IsBreakable Flag
Blocks are either breakable or not, specified by the `IsBreakable` flag. If a block is non-breakable it indicates to the parser that the close condition of any parent container do not apply so long as the non-breakable child block is still open.
The only built-in example of this is the `FencedCodeBlock`, which, if existing as the child of a container block of some sort, will prevent that container from being closed before the `FencedCodeBlock` is closed, since any characters inside the `FencedCodeBlock` are considered to be valid code and not the container's close condition.
#### RemoveAfterProcessInlines
## Inline Elements
Inlines in markdown refer to things like embellishments (italics, bold, underline, etc), links, urls, inline code, images, etc.
Inline elements may be one of two types:
1. `Inline`, whose parent is always a `ContainerInline`
2. `ContainerInline`, derived from `Inline`, which contains other inlines. `ContainerInline` also has a `ParentBlock` property of type `LeafBlock?`
**(Is there anything special worth documenting about inlines or types of inlines?)**
## The SourceSpan Struct
If the pipeline was configured with `.UsePreciseSourceLocation()`, all elements in the abstract syntax tree will contain a reference to the location in the original source where they occurred. This is done with the `SourceSpan` type, a custom Markdig `struct` which provides a start and end location.
All objects derived from `MarkdownObject` contain the `Span` property, which is of type `SourceSpan`.

127
doc/parsing-extensions.md Normal file
View File

@@ -0,0 +1,127 @@
# Extensions and Parsers
Markdig was [implemented in such a way](http://xoofx.com/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/) as to be extremely pluggable, with even basic behaviors being mutable and extendable.
The basic mechanism for extension of Markdig is the `IMarkdownExtension` interface, which allows any implementing class to be registered with the pipeline builder and thus to directly modify the collections of `BlockParser` and `InlineParser` objects which end up in the pipeline.
This document discusses the `IMarkdownExtension` interface, the `BlockParser` abstract base class, and the `InlineParser` abstract base class, which together are the foundation of extending Markdig's parsing machinery.
## Creating Extensions
Extensions can vary from very simple to very complicated.
A simple extension, for example, might simply find a parser already in the pipeline and modify a setting on it. An example of this is the `SoftlineBreakAsHardlineExtension`, which locates the `LineBreakInlineParser` and modifies a single boolean flag on it.
A complex extension, on the other hand, might add an entire taxonomy of new `Block` and `Inline` types, as well as several related parsers and renderers, and require being added to the the pipeline in a specific order in relation to other extensions which are already configured. The `FootnoteExtension` and `PipeTableExtension` are examples of more complex extensions.
For extensions that don't require order considerations, the implementation of the extension itself is adequate, and the extension can be added to the pipeline with the generic `Use<TExtension>()` method on the pipeline builder. For extensions which do require order considerations, it is best to create an extension method on the `MarkdownPipelineBuilder` to perform the registration. See the following two sections for further information.
### Implementation of an Extension
The [IMarkdownExtension.cs](https://github.com/xoofx/markdig/blob/master/src/Markdig/IMarkdownExtension.cs) interface specifies two methods which must be implemented.
The first, which takes only the pipeline builder as an argument, is called when the `Build()` method on the pipeline builder is invoked, and should set up any modifications to the parsers or parser collections. These parsers will then be used by the main parsing algorithm to process the source text.
```csharp
void Setup(MarkdownPipelineBuilder pipeline);
```
The second, which takes the pipeline itself and a renderer, is used to set up a rendering component in order to convert any special `MarkdownObject` types associated with the extension into an output. This is not relevant for parsing, but is necessary for rendering.
```csharp
void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer);
```
The extension can then be registered to the pipeline builder using the `Use<TExtension>()` method. A skeleton example is given below:
```csharp
public class MySpecialBlockParser : BlockParser
{
// ...
}
public class MyExtension : IMarkdownExtension
{
void Setup(MarkdownPipelineBuilder pipeline)
{
pipeline.BlockParsers.AddIfNotAlready<MySpecialBlockParser>();
}
void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) { }
}
```
```csharp
var builder = new MarkdownPipelineBuilder()
.Use<MyExtension>();
```
### Pipeline Builder Extension Methods
For extensions which require specific ordering and/or need to perform multiple operations to register with the builder, it's recommended to create an extension method.
```csharp
public static class MyExtensionMethods
{
public static MarkdownPipelineBuilder UseMyExtension(this MarkdownPipelineBuilder pipeline)
{
// Directly access or modify pipeline.Extensions here, with the ability to
// search for other extensions, insert before or after, remove other extensions,
// or modify their settings.
// ...
return pipeline;
}
}
```
### Simple Extension Example
An example of a simple extension which does not add any new parsers, but instead creates a new, horrific emphasis tag, marked by triple percentage signs. This example is based on [CitationExtension.cs](https://github.com/xoofx/markdig/blob/master/src/Markdig/Extensions/Citations/CitationExtension.cs)
```csharp
/// <summary>
/// An extension which applies to text of the form %%%text%%%
/// </summary>
public class BlinkExtension : IMarkdownExtension
{
// This setup method will be run when the pipeline builder's `Build()` method is invoked. As this
// is a simple, self-contained extension we won't be adding anything new, but rather finding an
// existing parser already in the pipeline and adding some settings to it.
public void Setup(MarkdownPipelineBuilder pipeline)
{
// We check the pipeline builder's inline parser collection and see if we can find a parser
// registered of the type EmphasisInlineParser. This is the parser which nominally handles
// bold and italic emphasis, but we know from its documentation that it is a general parser
// that can have new characters added to it.
var parser = pipeline.InlineParsers.FindExact<EmphasisInlineParser>();
// If we find the parser and it doesn't already have the % character registered, we add
// a descriptor for 3 consecutive % signs. This is specific to the EmphasisInlineParser and
// is just used here as an example.
if (parser is not null && !parser.HasEmphasisChar('%'))
{
parser.EmphasisDescriptors.Add(new EmphasisDescriptor('%', 3, 3, false));
}
}
// This method is called by the pipeline before rendering, which is a separate operation from
// parsing. This implementation is just here for the purpose of the example, in which we
// daisy-chain a delegate specific to the EmphasisInlineRenderer to cause an unconscionable tag
// to be inserted into the HTML output wherever a %%% annotated span was placed in the source.
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not HtmlRenderer) return;
var emphasisRenderer = renderer.ObjectRenderers.FindExact<EmphasisInlineRenderer>();
if (emphasisRenderer is null) return;
var previousTag = emphasisRenderer.GetTag;
emphasisRenderer.GetTag = inline =>
(inline.DelimiterCount == 3 && inline.DelimiterChar == '%' ? "blink" : null)
?? previousTag(inline);
}
}
```

336
doc/parsing-overview.md Normal file
View File

@@ -0,0 +1,336 @@
# Markdig Parsing
Markdig provides efficient, regex-free parsing of markdown documents directly into an abstract syntax tree (AST). The AST is a representation of the markdown document's semantic constructs, which can be manipulated and explored programmatically.
* This document contains a general overview of the parsing system and components and their use
* The [Abstract Syntax Tree](parsing-ast.md) document contains a discussion of how Markdig represents the product of the parsing operation
* The [Extensions/Parsers](parsing-extensions.md) document explores extensions and block/inline parsers within the context of extending Markdig's parsing capabilities
## Introduction
Markdig's parsing machinery consists of two main components at its surface: the `Markdown.Parse(...)` method and the `MarkdownPipeline` type. The parsed document is represented by a `MarkdownDocument` object, which is a tree of objects derived from `MarkdownObject`, including block and inline elements.
The `Markdown` static class is the main entrypoint to the Markdig API. It contains the `Parse(...)` method, the main algorithm for parsing a markdown document. The `Parse(...)` method in turn uses a `MarkdownPipeline`, which is a sealed internal class which maintains some configuration information and the collections of parsers and extensions. The `MarkdownPipeline` determines how the parser behaves and what its capabilities are. The `MarkdownPipeline` can be modified with built-in as well as user developed extensions.
### Glossary of Relevant Types
The following is a table of some of the types relevant to parsing and mentioned in the related documentation. For an exhaustive list refer to API documentation (coming soon).
|Type|Description|
|-|-|
|`Markdown`|Static class with the entry point to the parsing algorithm via the `Parse(...)` method|
|`MarkdownPipeline`|Configuration object for the parser, contains collections of block and inline parsers and registered extensions|
|`MarkdownPipelineBuilder`|Responsible for constructing the `MarkdownPipeline`, used by client code to configure pipeline options and behaviors|
|`IMarkdownExtension`|Interface for [Extensions](#extensions-imarkdownextension) which alter the behavior of the pipeline, this is the standard mechanism for extending Markdig|
|`BlockParser`|Base type for an individual parsing component meant to identify `Block` elements in the markdown source|
|`InlineParser`|Base type for an individual parsing component meant to identify `Inline` elements within a `Block`|
|`Block`|A node in the AST representing a markdown block element, can either be a `ContainerBlock` or a `LeafBlock`|
|`Inline`|A node in the AST representing a markdown inline element|
|`MarkdownDocument`|The root node of the AST produced by the parser, derived from `ContainerBlock`|
|`MarkdownObject`|The base type of all `Block` and `Inline` derived objects (as well as `HtmlAttributes`)|
### Simple Examples
*The following are simple examples of parsing to help get you started, see the following sections for an in-depth explanation of the different parts of Markdig's parsing mechanisms*
The `MarkdownPipeline` dictate how the parser will behave. The `Markdown.Parse(...)` method will construct a default pipeline if none is provided. A default pipeline will be CommonMark compliant but nothing else.
```csharp
var markdownText = File.ReadAllText("sample.md");
// No pipeline provided means a default pipeline will be used
var document = Markdown.Parse(markdownText);
```
Pipelines can be created and configured manually, however this must be done using a `MarkdownPipelineBuilder` object, which then is configured through a fluent interface composed of extension methods.
```csharp
var markdownText = File.ReadAllText("sample.md");
// Markdig's "UseAdvancedExtensions" option includes many common extensions beyond
// CommonMark, such as citations, figures, footnotes, grid tables, mathematics
// task lists, diagrams, and more.
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
var document = Markdown.Parse(markdownText, pipeline);
```
Extensions can also be added individually:
```csharp
var markdownText = File.ReadAllText("sample.md");
var pipeline = new MarkdownPipelineBuilder()
.UseCitations()
.UseFootnotes()
.UseMyCustomExtension()
.Build();
var document = Markdown.Parse(markdownText, pipeline);
```
## Markdown.Parse and the MarkdownPipeline
As metioned in the [Introduction](#introduction), Markdig's parsing machinery involves two surface components: the `Markdown.Parse(...)` method, and the `MarkdownPipeline` type. The main parsing algorithm (not to be confused with individual `BlockParser` and `InlineParser` components) lives in the `Markdown.Parse(...)` static method. The `MarkdownPipeline` is responsible for configuring the behavior of the parser.
These two components are covered in further detail in the following sections.
### The MarkdownPipeline
The `MarkdownPipeline` is a sealed internal class which dictates what features the parsing algorithm has. The pipeline must be created by using a `MarkdownPipelineBuilder` as shown in the examples above.
The `MarkdownPipeline` holds configuration information and collections of extensions and parsers. Parsers fall into one of two categories:
* Block Parsers (`BlockParser`)
* Inline Parsers (`InlineParser`)
Extensions are classes implementing `IMarkdownExtension` which are allowed to add to the list of parsers, or modify existing parsers and/or renderers. They are invoked to perform their mutations on the pipeline when the pipeline is built by the `MarkdownPipelineBuilder`.
Lastly, the `MarkdownPipeline` contains a few extra elements:
* A configuration setting determining whether or not trivial elements, referred to as *trivia*, (whitespace, extra heading characters, unescaped strings, etc) are to be tracked
* A configuration setting determining whether or not nodes in the resultant abstract syntax tree will refer to their precise original locations in the source
* An optional delegate which will be invoked when the document has been processed.
* An optional `TextWriter` which will get debug logging from the parser
### The Markdown.Parse Method
`Markdown.Parse` is a static method which contains the overall parsing algorithm but not the actual parsing components, which instead are contained within the pipeline.
The `Markdown.Parse(...)` method takes a string containing raw markdown and returns a `MarkdownDocument`, which is the root node in the abstract syntax tree. The `Parse(...)` method optionally takes a pre-configured `MarkdownPipeline`, but if none is given will create a default pipeline which has minimal features.
Within the `Parse(...)` method, the following sequence of operations occur:
1. The block parsers contained in the pipeline are invoked on the raw markdown text, creating the initial tree of block elements
2. If the pipeline is configured to track markdown trivia (trivial/non-contributing elements), the blocks are expanded to absorb neighboring trivia
3. The inline parsers contained in the pipeline are now invoked on the blocks, populating the inline elements of the abstract syntax tree
4. If a delegate has been configured for when the document has completed processing, it is now invoked
5. The abstract syntax tree (`MarkdownDocument` object) is returned
## The Pipeline Builder and Extensions
The `MarkdownPipeline` determines the behavior and capabilities of the parser, and *extensions* added via the `MarkdownPipelineBuilder` determine the configuration of the pipeline.
This section discusses the pipeline builder and the concept of *extensions* in more detail.
### Extensions (IMarkdownExtension)
***Note**: This section discusses how to consume extensions by adding them to pipeline. For a discussion on how to implement an extension, refer to the [Extensions/Parsers](parsing-extensions.md) document.*
Extensions are the primary mechanism for modifying the parsers in the pipeline.
An extension is any class which implements the `IMarkdownExtension` interface found in [IMarkdownExtension.cs](https://github.com/xoofx/markdig/blob/master/src/Markdig/IMarkdownExtension.cs). This interface consists solely of two `Setup(...)` overloads, which both take a `MarkdownPipelineBuilder` as the first argument.
When the `MarkdownPipelineBuilder.Build()` method is invoked as the final stage in pipeline construction, the builder runs through the list of registered extensions in order and calls the `Setup(...)` method on each of them. The extension then has full access to modify both the parser collections themselves (by adding new parsers to it), or to find and modify existing parsers.
Because of this, *some* extensions may need to be ordered in relation to others, for instance if they modify a parser that gets added by a different extension. The `OrderedList<T>` class contains convenience methods to this end, which aid in finding other extensions by type and then being able to added an item before or after them.
### The MarkdownPipelineBuilder
Because the `MarkdownPipeline` is a sealed internal class, it cannot (and *should* not be attempted to) be created directly. Rather, the `MarkdownPipelineBuilder` manages the requisite construction of the pipeline after the configuration has been provided by the client code.
As discussed in the [section above](#the-markdownpipeline), the `MarkdownPipeline` primarily consists of a collection of block parsers and a collection of inline parsers, which are provided to the `Markdown.Parse(...)` method and thus determine its features and behavior. Both the collections and some of the parsers themselves are mutable, and the mechanism of mutation is the `Setup(...)` method of the `IMarkdownExtension` interface. This is covered in more detail in the section on [Extensions](#extensions-imarkdownextension).
#### The Fluent Interface
A collection of extension methods in the [MarkdownExtensions.cs](https://github.com/xoofx/markdig/blob/master/src/Markdig/MarkdownExtensions.cs) source file provides a convenient fluent API for the configuration of the pipeline builder. This should be considered the standard way of configuring the builder.
##### Configuration Options
There are several extension methods which apply configurations to the builder which change settings in the pipeline outside of the use of typical extensions.
|Method|Description|
|-|-|
|`.ConfigureNewLine(...)`|Takes a string which will serve as the newline delimiter during parsing|
|`.DisableHeadings()`|Disables the parsing of ATX and Setex headings|
|`.DisableHtml()`|Disables the parsing of HTML elements|
|`.EnableTrackTrivia()`|Enables the tracking of trivia (trivial elements like whitespace)|
|`.UsePreciseSourceLocation()`|Maps syntax objects to their precise location in the original source, such as would be required for syntax highlighting|
```csharp
var builder = new MarkdownPipelineBuilder()
.ConfigureNewLine("\r\n")
.DisableHeadings()
.DisableHtml()
.EnableTrackTrivia()
.UsePreciseSourceLocation();
var pipeline = builder.Build();
```
##### Adding Extensions
All extensions which ship with Markdig can be added through a dedicated fluent method, while user code which implements the `IMarkdownExtension` interface can be added with one of the `Use()` methods, or via a custom extension method implemented in the client code.
Refer to [MarkdownExtensions.cs](https://github.com/xoofx/markdig/blob/master/src/Markdig/MarkdownExtensions.cs) for a full list of extension methods:
```csharp
var builder = new MarkdownPipelineBuilder()
.UseFootnotes()
.UseFigures();
```
For custom/user-provided extensions, the `Use<TExtension>(...)` methods allow either a type to be directly added or an already constructed instance to be put into the extension container. Internally they will prevent two of the same type of extension from being added to the container.
```csharp
public class MyExtension : IMarkdownExtension
{
// ...
}
// Only works if MyExtension has an empty constructor (aka new())
var builder = new MarkdownPipelineBuilder()
.Use<MyExtension>();
```
Alternatively:
```csharp
public class MyExtension : IMarkdownExtension
{
public MyExtension(object someConfigurationObject) { /* ... */ }
// ...
}
var instance = new MyExtension(configData);
var builder = new MarkdownPipelineBuilder()
.Use(instance);
```
##### Adding Extensions with the Configure Method
The `MarkdownPipelineBuilder` has one additional method for the configuration of extensions worth mentioning: the `Configure(...)` method, which takes a `string?` of `+` delimited tokens specifying which extensions should be dynamically configured. This is a convenience method for the configuration of pipelines whose extensions are only known at runtime.
Refer to [MarkdownExtensions.cs's `Configure(...)`](https://github.com/xoofx/markdig/blob/983187eace6ba02ee16d1443c387267ad6e78f58/src/Markdig/MarkdownExtensions.cs#L538) code for the full list of extensions.
```csharp
var builder = new MarkdownPipelineBuilder()
.Configure("common+footnotes+figures");
var pipeline = builder.Build();
```
#### Manual Configuration
Internally, the fluent interface wraps manual operations on the three primary collections:
* `MarkdownPipelineBuilder.BlockParsers` - this is an `OrderedList<BlockParser>` of the block parsers
* `MarkdownPipelineBuilder.InlineParsers` - this is an `OrderedList<InlineParser>` of the inline element parsers
* `MarkdownPipelineBuilder.Extensions` - this is an `OrderedList<IMarkdownExtension>` of the extensions
All three collections are `OrderedList<T>`, which is a collection type custom to Markdig which contains special methods for finding and inserting derived types. With the builder created, manual configuration can be performed by accessing these collections and their elements and modifying them as necessary.
***Warning**: be aware that it should not be necessary to directly modify either the `BlockParsers` or the `InlineParsers` collections directly during the pipeline configuration. Rather, these can and should be modified whenever possible through the `Setup(...)` method of extensions, which will be deferred until the pipeline is actually built and will allow for ordering such that operations dependent on other operations can be accounted for.*
## Block and Inline Parsers
Let's dive deeper into the parsing system. With a configured pipeline, the `Markdown.Parse` method will run through two two conceptual passes to produce the abstract syntax tree.
1. First, `BlockProcessor.ProcessLine` is called on the file's lines, one by one, trying to identify block elements in the source
2. Next, an `InlineProcessor` is created or borrowed and run on each block to identify inline elements.
These two conceptual operations dictate Markdig's two types of parsers, both of which derive from `ParserBase<TProcessor>`.
Block parsers, derived from `BlockParser`, identify block elements from lines in the source text and push them onto the abstract syntax tree. Inline parsers, derived from `InlineParser`, identify inline elements from `LeafBlock` elements and push them into an attached container: the `ContainerInline? LeafBlock.Inline` property.
Both inline and block parsers are regex-free, and instead work on finding opening characters and then making fast read-only views into the source text.
### Block Parser
**(The contents of this section I am very unsure of, this is from my reading of the code but I could use some guidance here)**
**(Does `CanInterrupt` specifically refer to interrupting a paragraph block?)**
In order to be added to the parsing pipeline, all block parsers must be derived from `BlockParser`.
Internally, the main parsing algorithm will be stepping through the source text, using the `HasOpeningCharacter(char c)` method of the block parser collection to pre-identify parsers which *could* be opening a block at a given position in the text based on the active character. Thus any derived implementation needs to set the value of the `char[]? OpeningCharacter` property with the initial characters that might begin the block.
If a parser can potentially open a block at a place in the source text it should expect to have the `TryOpen(BlockProcessor processor)` method called. This is a virtual method that must be implemented on any derived class. The `BlockProcessor` argument is a reference to an object which stores the current state of parsing and the position in the source.
**(What are the rules concerning how the `BlockState` return type should work for `TryOpen`? I see examples returning `None`, `Continue`, `BreakDiscard`, `ContinueDiscard`. How does the return value change the algorithm behavior?)**
**(Should a new block always be pushed into `processor.NewBlocks` in the `TryOpen` method?)**
As the main parsing algorithm moves forward, it will then call `TryContinue(...)` on blocks that were opened in `TryOpen(..)`.
**(Is this where/how you close a block? Is there anything that needs to be done to perform that beyond `block.UpdateSpanEnd` and returning `BlockState.Break`?)**
### Inline Parsers
Inline parsers extract inline markdown elements from the source, but their starting point is the text of each individual `LeafBlock` produced by the block parsing process. To understand the role of each inline parser it is necessary to first understand the inline parsing process as a whole.
#### The Inline Parsing Process
After the block parsing process has occurred, the abstract syntax tree of the document has been populated only with block elements, starting from the root `MarkdownDocument` node and ending with the individual `LeafBlock` derived block elements, most of which will be `ParagraphBlocks`, but also include things like `CodeBlocks`, `HeadingBlocks`, `FigureCaptions`, and so on.
At this point, the parsing machinery will iterate through each `LeafBlock` one by one, creating and assigning its `LeafBlock.Inline` property with an empty `ContainerInline`, and then sweeping through the `LeafBlock`'s text running the inline parsers. This occurs by the following process:
Starting at the first character of the text it will run through all of its `InlineParser` objects which have that character as a possible opening character for the type of inline they extract. The parsers will run in order (as such ordering is the *only* way which conflicts between parsers are resolved, and thus is important to the overall behavior of the parsing system) and the `Match(...)` method will be called on each candidate parser, in order, until one of them returns `true`.
The `Match(...)` method will be passed a slice of the text beginning at the *specific character* being processed and running until the end of the `LeafBlock`'s complete text. If the parser can create an `Inline` element it will do so and return `true`, otherwise it will return `false`. The parser will store the created `Inline` object in the processor's `InlineProcessor.Inline` property, which as passed into the `Match(...)` method as an argument. The parser will also advance the start of the working `StringSlice` by the characters consumed in the match.
* If the parser has created an inline element and returned `true`, that element is pushed into the deepest open `ContainerInline`
* If `false` was returned, a default `LiteralInlineParser` will run instead:
* If the `InlineProcessor.Inline` property already has an existing `LiteralInline` in it, these characters will be added to the existing `LiteralInline`, effectively growing it
* If no `LiteralInline` exists in the `InlineProcessor.Inline` property, a new one will be created containing the consumed characters and pushed into the deepest open `ContainerInline`
After that, the working text of the `LeafBlock` has been conceptually shortened by the advancing start of the working `StringSlice`, moving the starting character forward. If there is still text remaining, the process repeats from the new starting character until all of the text is consumed.
At this point, when all of the source text from the `LeafBlock` has been consumed, a post-processing step occurs. `InlineParser` objects in the pipeline which also implement `IPostInlineProcessor` are invoked on the `LeafBlock`'s root `ContainerInline`. This, for example, is the mechanism by which the unstructured output of the `EmphasisInlineParser` is then restructured into cleanly nested `EmphasisInline` and `LiteralInline` elements.
#### Responsibilities of an Inline Parser
Like the block parsers, an inline parser must provide an array of opening characters with the `char[]? OpeningCharacter` property.
However, inline parsers only require one other method, the `Match(InlineProcessor processor, ref StringSlice slice)` method, which is expected to determine if a match for the related inline is located at the starting character of the slice.
Within the `Match` method a parser should:
1. Determine if a match begins at the starting character of the `slice` argument
2. If no match exists, the method should return `false` and not advance the `Start` property of the `slice` argument
3. If a match does exist, perform the following actions:
* Instantiate the appropriate `Inline` derived class and assign it to the processor argument with `processor.Inline = myInlineObject`
* Advance the `Start` property of the `slice` argument by the number of characters contained in the match, for example by using the `NextChar()`, `SkipChar()`, or other helper methods of the `StringSlice` class
* Return `true`
While parsing, the `InlineProcessor` performing the processing, which is available to the `Match` function through the `processor` argument, contains a number of properties which can be used to access the current state of parsing. For example, the `processor.Inline` property is the mechanism for returning a new inline element, but before assignment it contains the last created inline, which in turn can be accessed for its parents.
Additionally, in the case of inlines which can be expected to contain other inlines, a possible strategy is to inject an inline element derived from `DelimiterInline` when the opening delimiter is detected, then to replace the opening delimiter with the final desired element when the closing delimiter is found. This is the strategy used by the `LinkInlineParser`, for example. In such cases the tools described in the next section, such as the `ReplaceBy` method, can be used. Note that if this method is used the post-processing should be invoked on the `InlineProcessor` in order to finalize any emphasis elements. For example, in the following code adapted from the `LinkInlineParser`:
```csharp
var parent = processor.Inline?.FirstParentOfType<MyDelimiterInline>();
if (parent is null) return;
var myInline = new MySpecialInline { /* set span and other parameters here */ };
// Replace the delimiter inline with the final inline type, adopting all of its children
parent.ReplaceBy(myInline);
// Notifies processor as we are creating an inline locally
processor.Inline = myInline;
// Process emphasis delimiters
processor.PostProcessInlines(0, myInline, null, false);
```
#### Inline Post-Processing
The purpose of post-processing inlines is typically to re-structure inline elements after the initial parsing is complete and the entire structure of the inline elements within a parent container is now available in a way it was not during the parsing process. Generally this consists of removing, replacing, and re-ordering `Inline` elements.
To this end, the `Inline` abstract base class contains several helper methods intended to allow manipulation of inline elements during the post-processing phase.
|Method|Purpose|
|-|-|
|`InsertAfter(...)`|Takes a new inline as an argument and inserts it into the same parent container after this instance|
|`InsertBefore(...)`|Takes a new inline as an argument and inserts it into the same parent container before this instance|
|`Remove()`|Removes this inline from its parent container|
|`ReplaceBy(...)`|Removes this instance and replaces it with a new inline specified in the argument. Has an option to move all of the original inline's children into the new inline.|
Additionally, the `PreviousSibling` and `NextSibling` properties can be used to determine the siblings of an inline element within its parent container. The `FirstParentOfType<T>()` method can be used to search for a parent element, which is often useful when searching for `DelimiterInline` derived elements, which are implemented as containers.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -14,7 +14,7 @@ You can **try Markdig online** and compare it to other implementations on [babel
- **Abstract Syntax Tree** with precise source code location for syntax tree, useful when building a Markdown editor.
- Checkout [MarkdownEditor for Visual Studio](https://visualstudiogallery.msdn.microsoft.com/eaab33c3-437b-4918-8354-872dfe5d1bfe) powered by Markdig!
- Converter to **HTML**
- Passing more than **600+ tests** from the latest [CommonMark specs (0.29)](http://spec.commonmark.org/)
- Passing more than **600+ tests** from the latest [CommonMark specs (0.30)](http://spec.commonmark.org/)
- Includes all the core elements of CommonMark:
- including **GFM fenced code blocks**.
- **Extensible** architecture
@@ -112,82 +112,34 @@ This software is released under the [BSD-Clause 2 license](https://github.com/lu
## Benchmarking
This is an early preview of the benchmarking against various implementations:
The latest benchmark was collected on April 23 2022, against the following implementations:
**C implementations**:
- [cmark](https://github.com/jgm/cmark) (version: 0.25.0): Reference C implementation of CommonMark, no support for extensions
- [Moonshine](https://github.com/brandonc/moonshine) (version: : popular C Markdown processor
**.NET implementations**:
- [Markdig](https://github.com/lunet-io/markdig) (version: 0.5.x): itself
- [CommonMark.NET(master)](https://github.com/Knagis/CommonMark.NET) (version: 0.11.0): CommonMark implementation for .NET, no support for extensions, port of cmark
- [CommonMark.NET(pipe_tables)](https://github.com/AMDL/CommonMark.NET/tree/pipe-tables): An evolution of CommonMark.NET, supports extensions, not released yet
- [MarkdownDeep](https://github.com/toptensoftware/markdowndeep) (version: 1.5.0): another .NET implementation
- [MarkdownSharp](https://github.com/Kiri-rin/markdownsharp) (version: 1.13.0): Open source C# implementation of Markdown processor, as featured on Stack Overflow, regexp based.
- [Marked.NET](https://github.com/T-Alex/MarkedNet) (version: 1.0.5) port of original [marked.js](https://github.com/chjj/marked) project
- [Microsoft.DocAsCode.MarkdownLite](https://github.com/dotnet/docfx/tree/dev/src/Microsoft.DocAsCode.MarkdownLite) (version: 2.0.1) used by the [docfx](https://github.com/dotnet/docfx) project
### Analysis of the results:
- Markdig is roughly **x100 times faster than MarkdownSharp**, **30x times faster than docfx**
- **Among the best in CPU**, Extremely competitive and often faster than other implementations (not feature wise equivalent)
- **15% to 30% less allocations** and GC pressure
Because Marked.NET, MarkdownSharp and DocAsCode.MarkdownLite are way too slow, they are not included in the following charts:
![BenchMark CPU Time](img/BenchmarkCPU.png)
![BenchMark Memory](img/BenchmarkMemory.png)
### Performance for x86:
- [Markdig](https://github.com/lunet-io/markdig) (version: 0.30.2): itself
- [cmark](https://github.com/commonmark/cmark) (version: 0.30.2): Reference C implementation of CommonMark, no support for extensions
- [CommonMark.NET(master)](https://github.com/Knagis/CommonMark.NET) (version: 0.15.1): CommonMark implementation for .NET, no support for extensions, port of cmark, deprecated.
- [MarkdownSharp](https://github.com/Kiri-rin/markdownsharp) (version: 2.0.5): Open source C# implementation of Markdown processor, as featured previously on Stack Overflow, regexp based.
```
BenchmarkDotNet-Dev=v0.9.7.0+
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-4770 CPU 3.40GHz, ProcessorCount=8
Frequency=3319351 ticks, Resolution=301.2637 ns, Timer=TSC
HostCLR=MS.NET 4.0.30319.42000, Arch=32-bit RELEASE
JitModules=clrjit-v4.6.1080.0
// * Summary *
Type=Program Mode=SingleRun LaunchCount=2
WarmupCount=2 TargetCount=10
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=6.0.202
[Host] : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
DefaultJob : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
Method | Median | StdDev |Scaled | Gen 0 | Gen 1| Gen 2|Bytes Allocated/Op |
--------------------------- |------------ |---------- |------ | ------ |------|---------|------------------ |
Markdig | 5.5316 ms | 0.0372 ms | 0.71 | 56.00| 21.00| 49.00| 1,285,917.31 |
CommonMark.NET(master) | 4.7035 ms | 0.0422 ms | 0.60 | 113.00| 7.00| 49.00| 1,502,404.60 |
CommonMark.NET(pipe_tables) | 5.6164 ms | 0.0298 ms | 0.72 | 111.00| 56.00| 49.00| 1,863,128.13 |
MarkdownDeep | 7.8193 ms | 0.0334 ms | 1.00 | 120.00| 56.00| 49.00| 1,884,854.85 |
cmark | 4.2698 ms | 0.1526 ms | 0.55 | -| -| -| NA |
Moonshine | 6.0929 ms | 0.1053 ms | 1.28 | -| -| -| NA |
Marked.NET | 207.3169 ms | 5.2628 ms | 26.51 | 0.00| 0.00| 0.00| 303,125,228.65 |
MarkdownSharp | 675.0185 ms | 2.8447 ms | 86.32 | 40.00| 27.00| 41.00| 2,413,394.17 |
Microsoft DocfxMarkdownLite | 166.3357 ms | 0.4529 ms | 21.27 |4,452.00|948.00|11,167.00| 180,218,359.60 |
| Method | Mean | Error | StdDev |
|------------------ |-----------:|----------:|----------:|
| markdig | 1.979 ms | 0.0221 ms | 0.0185 ms |
| cmark | 2.571 ms | 0.0081 ms | 0.0076 ms |
| CommonMark.NET | 2.016 ms | 0.0169 ms | 0.0158 ms |
| MarkdownSharp | 221.455 ms | 1.4442 ms | 1.3509 ms |
```
### Performance for x64:
- Markdig is roughly **x100 times faster than MarkdownSharp**
- **20% faster than the reference cmark C implementation**
```
BenchmarkDotNet-Dev=v0.9.6.0+
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz, ProcessorCount=8
Frequency=3319351 ticks, Resolution=301.2637 ns, Timer=TSC
HostCLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT]
JitModules=clrjit-v4.6.1080.0
Type=Program Mode=SingleRun LaunchCount=2
WarmupCount=2 TargetCount=10
Method | Median | StdDev | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op |
--------------------- |---------- |---------- |------- |------- |------ |------------------- |
TestMarkdig | 5.5276 ms | 0.0402 ms | 109.00 | 96.00 | 84.00 | 1,537,027.66 |
TestCommonMarkNet | 4.4661 ms | 0.1190 ms | 157.00 | 96.00 | 84.00 | 1,747,432.06 |
TestCommonMarkNetNew | 5.3151 ms | 0.0815 ms | 229.00 | 168.00 | 84.00 | 2,323,922.97 |
TestMarkdownDeep | 7.4076 ms | 0.0617 ms | 318.00 | 186.00 | 84.00 | 2,576,728.69 |
```
## Donate

162
site/.lunet/css/main.css Normal file
View File

@@ -0,0 +1,162 @@
/* main */
:root {
--base-html-font-size: 62.5%;
--base-font-size: 1.65rem;
--container-xl-max-width: 160rem;
--site-logo-size: 45px;
--site-logo-url: url("/img/markdig.png");
}
/* make tocbot working with halfmoon */
html, body, .page-wrapper, .content-wrapper {
position: static;
}
.page-wrapper {
overflow: visible;
}
/* disable any box around active headers with tocbot */
h1:focus, h1:active,
h2:focus, h2:active,
h3:focus, h3:active,
h4:focus, h4:active,
h5:focus, h5:active,
h6:focus, h6:active
{
outline: none;
}
.nav-item.active {
border-bottom-width: 2px;
border-bottom-style: solid;
border-bottom-color: var(--lm-navbar-link-active-text-color);
}
.main-row {
min-height: 92vh;
}
.nav-item.active .nav-link {
color: var(--lm-navbar-link-active-text-color);
background-color: var(--lm-navbar-link-active-bg-color);
}
.site-logo {
background: transparent var(--site-logo-url) no-repeat scroll 0px 0px / var(--site-logo-size) var(--site-logo-size);
display: block;
width: var(--site-logo-size);
height: var(--site-logo-size);
}
.in-this-article-nav {
position: fixed;
margin-right: 2rem;
z-index: 20;
padding: 0.5rem;
border-radius: 0.4rem;
}
.in-this-article-nav .title {
font-weight: bold;
font-size: 1em;
padding-bottom: 1em;
}
.content .is-active-link::before {
background-color: var(--primary-color);
}
.heading-anchor-hidden {
display: none !important;
}
@media (min-width: 1600px) {
html {
font-size: var(--base-html-font-size);
}
}
@media (min-width: 1920px) {
html {
font-size: var(--base-html-font-size);
}
}
table.api-dotnet-inherit, table.api-dotnet-implements, table.api-dotnet-derived {
margin-bottom: 1em;
}
.api-dotnet-inherit td, .api-dotnet-implements td, .api-dotnet-derived td {
vertical-align: top;
}
.api-dotnet-inherit div:not(:first-child):before {
content: " ᐅ ";
white-space: pre;
}
/*.api-dotnet-inherit td, .api-dotnet-inherit tr, .api-dotnet-implements td, .api-dotnet-implements tr, .api-dotnet-derived tr {
display: flex;
}
*/
.api-dotnet-inherit div, .api-dotnet-implements div
{
display: inline-block;
}
.api-dotnet-inherit td:not(:first-child), .api-dotnet-implements td:not(:first-child), .api-dotnet-derived td:not(:first-child) {
padding-left: 1em;
}
.api-dotnet-implements div:not(:first-child):before {
content: ", ";
white-space: pre;
}
.api-dotnet-parameter-list {
display: flex;
}
.api-dotnet-parameter-list dd {
margin-left: 1em;
}
dl.api-dotnet-parameter-list {
margin-bottom: 0px;
}
table.api-dotnet-members-table {
width: 100%;
}
table.api-dotnet-members-table p {
margin-top: 0px;
margin-bottom: 0px;
}
.api-dotnet-members-table tr td:first-child {
width: 33%;
}
div.api-dotnet {
width: 100%;
}
.api-dotnet-members-table tr:not(:first-child) {
border-top: 1px solid #ddd;
}
.api-dotnet-members-table td {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
@media (prefers-color-scheme: dark) {
.api-dotnet-members-table tr:not(:first-child) {
border-top: 1px solid #777;
}
}

701
site/.lunet/css/prism.css Normal file
View File

@@ -0,0 +1,701 @@
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism-coy&languages=markup+clike+c+csharp+cpp+fsharp+markdown&plugins=line-highlight+line-numbers+toolbar+show-language */
/**
* prism.js Coy theme for JavaScript, CoffeeScript, CSS and HTML
* Based on https://github.com/tshedor/workshop-wp-theme (Example: http://workshop.kansan.com/category/sessions/basics or http://workshop.timshedor.com/category/sessions/basics);
* @author Tim Shedor
*/
@media (prefers-color-scheme: light) {
code[class*="language-"],
pre[class*="language-"] {
color: #393A34;
background: none;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre-wrap;
word-spacing: normal;
word-break: break-word;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
position: relative;
margin: .5em 0;
overflow: visible;
padding: 0;
}
pre[class*="language-"]>code {
position: relative;
border-left: 4px solid #358ccb;
box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf;
background-color: #fdfdfd;
background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%);
background-size: 3em 3em;
background-origin: content-box;
background-attachment: local;
}
code[class*="language"] {
max-height: inherit;
height: inherit;
padding: 0 1em;
display: block;
overflow: auto;
}
/* Margin bottom to accommodate shadow */
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background-color: #fdfdfd;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
margin-bottom: 1em;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
position: relative;
padding: .2em;
border-radius: 0.3em;
color: #c92c2c;
border: 1px solid #dddddd;
display: inline;
white-space: normal;
}
pre[class*="language-"]:before,
pre[class*="language-"]:after {
content: '';
z-index: -2;
display: block;
position: absolute;
bottom: 0.75em;
left: 0.18em;
width: 40%;
height: 20%;
max-height: 13em;
}
:not(pre) > code[class*="language-"]:after,
pre[class*="language-"]:after {
right: 0.75em;
left: auto;
-webkit-transform: rotate(2deg);
-moz-transform: rotate(2deg);
-ms-transform: rotate(2deg);
-o-transform: rotate(2deg);
transform: rotate(2deg);
}
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #008000; font-style: italic;
}
.token.char,
.token.string {
color: #A31515;
}
.token.punctuation {
color: #393A34; /* no highlight */
}
.token.deleted {
color: #c92c2c;
}
.token.function-name,
.token.function {
color: #393A34;
}
.token.tag,
.token.selector {
color: #800000;
}
.token.attr-name,
.token.regex,
.token.entity {
color: #ff0000;
}
.token.builtin,
.token.url,
.token.symbol,
.token.number,
.token.boolean,
.token.variable,
.token.constant,
.token.inserted {
color: #36acaa;
}
.token.operator,
.token.variable {
color: #a67f59;
background: rgba(255, 255, 255, 0.5);
}
.token.atrule,
.token.attr-value,
.token.keyword,
.token.class-name {
color: #0000ff;
}
.token.directive.tag .tag {
background: #ffff00;
color: #393A34;
}
.token.class-name,
.token.property {
color: #2B91AF;
}
.token.important {
color: #e90;
}
.language-css .token.string,
.style .token.string {
color: #a67f59;
background: rgba(255, 255, 255, 0.5);
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.namespace {
opacity: .7;
}
@media screen and (max-width: 767px) {
pre[class*="language-"]:before,
pre[class*="language-"]:after {
bottom: 14px;
box-shadow: none;
}
}
/* Plugin styles */
.token.tab:not(:empty):before,
.token.cr:before,
.token.lf:before {
color: #e0d7d1;
}
/* Plugin styles: Line Numbers */
pre[class*="language-"].line-numbers.line-numbers {
padding-left: 0;
}
pre[class*="language-"].line-numbers.line-numbers code {
padding-left: 3.8em;
}
pre[class*="language-"].line-numbers.line-numbers .line-numbers-rows {
left: 0;
}
/* Plugin styles: Line Highlight */
pre[class*="language-"][data-line] {
padding-top: 0;
padding-bottom: 0;
padding-left: 0;
}
pre[data-line] code {
position: relative;
padding-left: 4em;
}
pre .line-highlight {
margin-top: 0;
}
pre[data-line] {
position: relative;
padding: 1em 0 1em 3em;
}
.line-highlight {
position: absolute;
left: 0;
right: 0;
padding: inherit 0;
margin-top: 1em; /* Same as .prisms padding-top */
background: hsla(24, 20%, 50%,.08);
background: linear-gradient(to right, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0));
pointer-events: none;
line-height: inherit;
white-space: pre;
}
.line-highlight:before,
.line-highlight[data-end]:after {
content: attr(data-start);
position: absolute;
top: .4em;
left: .6em;
min-width: 1em;
padding: 0 .5em;
background-color: hsla(24, 20%, 50%,.4);
color: hsl(24, 20%, 95%);
font: bold 65%/1.5 sans-serif;
text-align: center;
vertical-align: .3em;
border-radius: 999px;
text-shadow: none;
box-shadow: 0 1px white;
}
.line-highlight[data-end]:after {
content: attr(data-end);
top: auto;
bottom: .4em;
}
.line-numbers .line-highlight:before,
.line-numbers .line-highlight:after {
content: none;
}
pre[class*="language-"].line-numbers {
position: relative;
padding-left: 3.8em;
counter-reset: linenumber;
}
pre[class*="language-"].line-numbers > code {
position: relative;
white-space: inherit;
}
.line-numbers .line-numbers-rows {
position: absolute;
pointer-events: none;
top: 0;
font-size: 100%;
left: -3.8em;
width: 3em; /* works for line-numbers below 1000 lines */
letter-spacing: -1px;
border-right: 1px solid #a5a5a5;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.line-numbers-rows > span {
pointer-events: none;
display: block;
counter-increment: linenumber;
}
.line-numbers-rows > span:before {
content: counter(linenumber);
color: #2B91AF;
display: block;
padding-right: 0.8em;
text-align: right;
}
div.code-toolbar {
position: relative;
}
div.code-toolbar > .toolbar {
position: absolute;
top: .3em;
right: .2em;
transition: opacity 0.3s ease-in-out;
opacity: 0;
}
div.code-toolbar:hover > .toolbar {
opacity: 1;
}
div.code-toolbar > .toolbar .toolbar-item {
display: inline-block;
}
div.code-toolbar > .toolbar a {
cursor: pointer;
}
div.code-toolbar > .toolbar button {
background: none;
border: 0;
color: inherit;
font: inherit;
line-height: normal;
overflow: visible;
padding: 0;
-webkit-user-select: none; /* for button */
-moz-user-select: none;
-ms-user-select: none;
}
div.code-toolbar > .toolbar a,
div.code-toolbar > .toolbar button,
div.code-toolbar > .toolbar span {
color: #bbb;
font-size: .8em;
padding: 0 .5em;
background: #f5f2f0;
background: rgba(224, 224, 224, 0.2);
box-shadow: 0 2px 0 0 rgba(0,0,0,0.2);
border-radius: .5em;
}
div.code-toolbar > .toolbar a:hover,
div.code-toolbar > .toolbar a:focus,
div.code-toolbar > .toolbar button:hover,
div.code-toolbar > .toolbar button:focus,
div.code-toolbar > .toolbar span:hover,
div.code-toolbar > .toolbar span:focus {
color: inherit;
text-decoration: none;
}
}
@media (prefers-color-scheme: dark) {
/* https://github.com/PrismJS/prism-themes/blob/master/themes/prism-vsc-dark-plus.css */
pre[class*="language-"],
code[class*="language-"] {
color: #d4d4d4;
font-size: 13px;
text-shadow: none;
font-family: Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace;
direction: ltr;
text-align: left;
white-space: pre-wrap;
word-spacing: normal;
word-break: break-word;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"] {
position: relative;
margin: .5em 0;
overflow: visible;
background: #1e1e1e;
}
pre[class*="language-"]>code {
border-left: 4px solid #358ccb;
}
code[class*="language"] {
max-height: inherit;
height: inherit;
padding: 0.5em 1em;
display: block;
overflow: auto;
}
pre[class*="language-"]::selection,
code[class*="language-"]::selection,
pre[class*="language-"] *::selection,
code[class*="language-"] *::selection {
text-shadow: none;
background: #75a7ca;
}
@media print {
pre[class*="language-"],
code[class*="language-"] {
text-shadow: none;
}
}
:not(pre) > code[class*="language-"] {
padding: .1em .3em;
border-radius: .3em;
color: #db4c69;
background: #f9f2f4;
}
/*********************************************************
* Tokens
*/
.namespace {
opacity: .7;
}
.token.doctype .token.doctype-tag {
color: #569CD6;
}
.token.doctype .token.name {
color: #9cdcfe;
}
.token.comment,
.token.prolog {
color: #6a9955;
}
.token.punctuation,
.language-html .language-css .token.punctuation,
.language-html .language-javascript .token.punctuation {
color: #d4d4d4;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.inserted,
.token.unit {
color: #b5cea8;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.deleted {
color: #ce9178;
}
.language-css .token.string.url {
text-decoration: underline;
}
.token.operator,
.token.entity {
color: #d4d4d4;
}
.token.operator.arrow {
color: #569CD6;
}
.token.atrule {
color: #ce9178;
}
.token.atrule .token.rule {
color: #c586c0;
}
.token.atrule .token.url {
color: #9cdcfe;
}
.token.atrule .token.url .token.function {
color: #dcdcaa;
}
.token.atrule .token.url .token.punctuation {
color: #d4d4d4;
}
.token.keyword {
color: #569CD6;
}
.token.keyword.module,
.token.keyword.control-flow {
color: #c586c0;
}
.token.function,
.token.function .token.maybe-class-name {
color: #dcdcaa;
}
.token.regex {
color: #d16969;
}
.token.important {
color: #569cd6;
}
.token.italic {
font-style: italic;
}
.token.constant {
color: #9cdcfe;
}
.token.class-name,
.token.maybe-class-name {
color: #4ec9b0;
}
.token.console {
color: #9cdcfe;
}
.token.parameter {
color: #9cdcfe;
}
.token.interpolation {
color: #9cdcfe;
}
.token.punctuation.interpolation-punctuation {
color: #569cd6;
}
.token.boolean {
color: #569cd6;
}
.token.property,
.token.variable,
.token.imports .token.maybe-class-name,
.token.exports .token.maybe-class-name {
color: #9cdcfe;
}
.token.selector {
color: #d7ba7d;
}
.token.escape {
color: #d7ba7d;
}
.token.tag {
color: #569cd6;
}
.token.tag .token.punctuation {
color: #808080;
}
.token.cdata {
color: #808080;
}
.token.attr-name {
color: #9cdcfe;
}
.token.attr-value,
.token.attr-value .token.punctuation {
color: #ce9178;
}
.token.attr-value .token.punctuation.attr-equals {
color: #d4d4d4;
}
.token.entity {
color: #569cd6;
}
.token.namespace {
color: #4ec9b0;
}
/*********************************************************
* Language Specific
*/
pre[class*="language-javascript"],
code[class*="language-javascript"],
pre[class*="language-jsx"],
code[class*="language-jsx"],
pre[class*="language-typescript"],
code[class*="language-typescript"],
pre[class*="language-tsx"],
code[class*="language-tsx"] {
color: #9cdcfe;
}
pre[class*="language-css"],
code[class*="language-css"] {
color: #ce9178;
}
pre[class*="language-html"],
code[class*="language-html"] {
color: #d4d4d4;
}
.language-regex .token.anchor {
color: #dcdcaa;
}
.language-html .token.punctuation {
color: #808080;
}
/*********************************************************
* Line highlighting
*/
pre[data-line] {
position: relative;
}
pre[class*="language-"] > code[class*="language-"] {
position: relative;
z-index: 1;
}
.line-highlight {
position: absolute;
left: 0;
right: 0;
padding: inherit 0;
margin-top: 1em;
background: #f7ebc6;
box-shadow: inset 5px 0 0 #f7d87c;
z-index: 0;
pointer-events: none;
line-height: inherit;
white-space: pre;
}
}

24
site/.lunet/js/xoofx.js Normal file
View File

@@ -0,0 +1,24 @@
anchors.add();
var jstoc = document.getElementsByClassName("js-toc");
if (jstoc.length > 0)
{
tocbot.init({
// Which headings to grab inside of the contentSelector element.
headingSelector: 'h2, h3, h4, h5',
collapseDepth: 3,
orderedList: true,
hasInnerContainers: true,
});
}
(function () {
const InitLunetTheme = function (e) {
if (halfmoon.darkModeOn != e.matches) {
halfmoon.toggleDarkMode();
}
}
let colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: dark)')
InitLunetTheme(colorSchemeQueryList);
colorSchemeQueryList.addListener(InitLunetTheme);
})();

View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en" itemscope itemtype="http://schema.org/WebPage" class="my-html-setup">
<head>
{{~ Head ~}}
</head>
<body class="with-custom-webkit-scrollbars with-custom-css-scrollbars">
<div class="page-wrapper with-transitions">
<div class="sticky-alerts"></div>
<div class="content-wrapper bg-light-lm bg-dark-dm">
<div class="container-xl bg-white-lm bg-dark-light-dm">
<div class="row">
<div class="col-xl-12">
<nav class="navbar">
<a class="site-logo navbar-brand" href="/"></a>
<div id="navbarSupportedContent" class="collapse navbar-collapse justify-content-between">
{{~ site.menu.home.render {
depth: 1,
kind: "nav",
list_class: "w-100"
}
~}}
</div>
</nav>
</div>
</div>
<div class="row main-row">
{{~ $main_content_col_size = 8 ~}}
{{~ if page.nomenu; $main_content_col_size = $main_content_col_size + 2; end }}
{{~ if page.notoc; $main_content_col_size = $main_content_col_size + 2; end }}
{{~ if !page.nomenu ~}}
<div class="col-xl-2 ">
<div class="content">
Menu
</div>
</div>
{{~ end ~}}
<div class="col-xl-{{ $main_content_col_size }} ">
<div class="content js-toc-content">
{{ content }}
</div>
</div>
{{~ if !page.notoc ~}}
<div class="col-xl-2 ">
<div class="content">
<div class="in-this-article-nav">
<div class="title">In this article</div>
<nav class="js-toc toc"></nav>
</div>
</div>
</div>
{{~ end ~}}
</div>
<div class="row">
<div class="col-xl-12">
<nav class="navbar navbar-fixed-bottom justify-content-center">
<footer>Copyright &copy; 2009 - {{ date.now.year }}, Alexandre Mutel - Blog content licensed under the Creative Commons <a href="http://creativecommons.org/licenses/by/2.5/">CC BY 2.5</a> | Site powered by <a href="https://github.com/lunet-io/lunet">lunet</a></footer>
</nav>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,5 @@
---
layout: _default
---
<h1 class="title">{{ page.title_html ?? page.title }}</h1>
{{ content }}

70
site/config.scriban Normal file
View File

@@ -0,0 +1,70 @@
# Site Settings
author = "Alexandre Mutel"
title = "markdig"
description = "markdig website"
html.head.title = do; ret page.title + " | " + site.title; end
basepath = ""
baseurl = baseurl ?? "https://markdig.net"
# Github repository
github_user = "xoofx"
github_repo_url = "https://github.com/lunet-io/markdig/"
with cards.twitter
enable = true
card = "summary_large_image"
user = "markdig"
image = "/images/twitter-banner.png"
end
# Resources bundle
with bundle
fontawesome = resource "npm:font-awesome" "4.7.0"
halfmoon = resource "npm:halfmoon" "1.1.0"
tocbot = resource "npm:tocbot" "4.12.1"
anchorjs = resource "npm:anchor-js" "4.2.2"
prismjs = resource "npm:prismjs" "1.20.0"
# scss.includes.add fontawesome.path + "/scss"
# css files
css halfmoon "/css/halfmoon-variables.min.css"
css tocbot "/dist/tocbot.css"
css fontawesome "/css/font-awesome.min.css"
css "/css/prism.css"
css "/css/main.css"
# js files
js anchorjs "/anchor.min.js"
js halfmoon "/js/halfmoon.min.js"
js tocbot "/dist/tocbot.min.js"
js prismjs "/prism.js"
js prismjs "/components/prism-shell-session.min.js"
js prismjs "/components/prism-clike.min.js"
js prismjs "/components/prism-c.min.js"
js prismjs "/components/prism-cpp.min.js"
js prismjs "/components/prism-csharp.min.js"
js "/js/prism-stark.js"
js "/js/xoofx.js"
# copy font files
content fontawesome "/fonts/fontawesome-webfont.*" "/fonts/"
# concatenate css/js files
concat = true
minify = true
end
with attributes
# match "/blog/**/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]*.md" {
# url: "/:section/:year/:month/:day/:title:output_ext"
#}
end
with api.dotnet
title = "Markdig .NET API Reference"
projects = ["../src/Markdig/Markdig.csproj"]
properties = {TargetFramework: "netstandard2.0"}
end

BIN
site/img/markdig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

3
site/menu.yml Normal file
View File

@@ -0,0 +1,3 @@
home:
- {path: readme.md}
- {path: api/readme.md, title: "Markdig API"}

10
site/readme.md Normal file
View File

@@ -0,0 +1,10 @@
---
layout: simple
title: Home
title_html: Hi and Welcome!
---
Hello World!
Test: <xref:system.object>

8
site/system.md Normal file
View File

@@ -0,0 +1,8 @@
---
uid: system.object
layout: simple
title: Home
title_html: Hi and Welcome!
---
Hello World from system.object!

View File

@@ -8,29 +8,11 @@
<ItemGroup>
<None Remove="spec.md" />
</ItemGroup>
<ItemGroup>
<Reference Include="CommonMarkNew, Version=0.1.0.0, Culture=neutral, PublicKeyToken=001ef8810438905d, processorArchitecture=MSIL">
<HintPath>lib\CommonMarkNew.dll</HintPath>
<SpecificVersion>False</SpecificVersion>
<Aliases>newcmark</Aliases>
<Private>True</Private>
</Reference>
<Reference Include="MoonShine">
<HintPath>lib\MoonShine.dll</HintPath>
</Reference>
<Reference Include="MarkdownDeep">
<HintPath>lib\MarkdownDeep.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Content Include="cmark.dll">
<HintPath>cmark.dll</HintPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="libsundown.dll">
<HintPath>libsundown.dll</HintPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="spec.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -39,9 +21,9 @@
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.1" />
<PackageReference Include="CommonMark.NET" Version="0.15.1" />
<PackageReference Include="Markdown" Version="2.2.1" />
<PackageReference Include="MarkdownSharp" Version="2.0.5" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="2.0.226801" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.74" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Markdig\Markdig.csproj" />

View File

@@ -1,9 +1,7 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// 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.
extern alias newcmark;
using System;
using System.Diagnostics;
using System.IO;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
@@ -25,7 +23,7 @@ namespace Testamina.Markdig.Benchmarks
}
//[Benchmark(Description = "TestMarkdig", OperationsPerInvoke = 4096)]
[Benchmark]
[Benchmark(Description = "markdig")]
public void TestMarkdig()
{
//var reader = new StreamReader(File.Open("spec.md", FileMode.Open));
@@ -33,7 +31,7 @@ namespace Testamina.Markdig.Benchmarks
//File.WriteAllText("spec.html", writer.ToString());
}
[Benchmark]
[Benchmark(Description = "cmark")]
public void TestCommonMarkCpp()
{
//var reader = new StreamReader(File.Open("spec.md", FileMode.Open));
@@ -41,7 +39,7 @@ namespace Testamina.Markdig.Benchmarks
//File.WriteAllText("spec.html", writer.ToString());
}
[Benchmark]
[Benchmark(Description = "CommonMark.NET")]
public void TestCommonMarkNet()
{
////var reader = new StreamReader(File.Open("spec.md", FileMode.Open));
@@ -55,93 +53,25 @@ namespace Testamina.Markdig.Benchmarks
//writer.ToString();
}
[Benchmark]
public void TestCommonMarkNetNew()
{
////var reader = new StreamReader(File.Open("spec.md", FileMode.Open));
// var reader = new StringReader(text);
//CommonMark.CommonMarkConverter.Parse(reader);
//CommonMark.CommonMarkConverter.Parse(reader);
//reader.Dispose();
//var writer = new StringWriter();
newcmark::CommonMark.CommonMarkConverter.Convert(text);
//writer.Flush();
//writer.ToString();
}
[Benchmark]
public void TestMarkdownDeep()
{
new MarkdownDeep.Markdown().Transform(text);
}
[Benchmark]
[Benchmark(Description = "MarkdownSharp")]
public void TestMarkdownSharp()
{
new MarkdownSharp.Markdown().Transform(text);
}
[Benchmark]
public void TestMoonshine()
{
Sundown.MoonShine.Markdownify(text);
}
static void Main(string[] args)
{
bool markdig = args.Length == 0;
bool simpleBench = false;
var config = ManualConfig.Create(DefaultConfig.Instance);
//var gcDiagnoser = new MemoryDiagnoser();
//config.Add(new Job { Mode = Mode.SingleRun, LaunchCount = 2, WarmupCount = 2, IterationTime = 1024, TargetCount = 10 });
//config.Add(new Job { Mode = Mode.Throughput, LaunchCount = 2, WarmupCount = 2, TargetCount = 10 });
//config.Add(gcDiagnoser);
if (simpleBench)
{
var clock = Stopwatch.StartNew();
var program = new Program();
GC.Collect(2, GCCollectionMode.Forced, true);
var gc0 = GC.CollectionCount(0);
var gc1 = GC.CollectionCount(1);
var gc2 = GC.CollectionCount(2);
const int count = 12*64;
for (int i = 0; i < count; i++)
{
if (markdig)
{
program.TestMarkdig();
}
else
{
program.TestCommonMarkNetNew();
}
}
clock.Stop();
Console.WriteLine((markdig ? "MarkDig" : "CommonMark") + $" => time: {(double)clock.ElapsedMilliseconds/count}ms (total {clock.ElapsedMilliseconds}ms)");
DumpGC(gc0, gc1, gc2);
}
else
{
//new TestMatchPerf().TestMatch();
var config = ManualConfig.Create(DefaultConfig.Instance);
//var gcDiagnoser = new MemoryDiagnoser();
//config.Add(new Job { Mode = Mode.SingleRun, LaunchCount = 2, WarmupCount = 2, IterationTime = 1024, TargetCount = 10 });
//config.Add(new Job { Mode = Mode.Throughput, LaunchCount = 2, WarmupCount = 2, TargetCount = 10 });
//config.Add(gcDiagnoser);
//var config = DefaultConfig.Instance;
BenchmarkRunner.Run<Program>(config);
//BenchmarkRunner.Run<TestDictionary>(config);
//BenchmarkRunner.Run<TestMatchPerf>();
//BenchmarkRunner.Run<TestStringPerf>();
}
}
private static void DumpGC(int gc0, int gc1, int gc2)
{
Console.WriteLine($"gc0: {GC.CollectionCount(0)-gc0}");
Console.WriteLine($"gc1: {GC.CollectionCount(1)-gc1}");
Console.WriteLine($"gc2: {GC.CollectionCount(2)-gc2}");
//var config = DefaultConfig.Instance;
BenchmarkRunner.Run<Program>(config);
//BenchmarkRunner.Run<TestDictionary>(config);
//BenchmarkRunner.Run<TestMatchPerf>();
//BenchmarkRunner.Run<TestStringPerf>();
}
}
}

Binary file not shown.

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
@@ -10,13 +10,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.msbuild" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
</ItemGroup>
<ItemGroup>
@@ -32,7 +28,7 @@
<InputSpecFiles Include="RoundtripSpecs\*.md" />
<InputSpecFiles Remove="Specs\readme.md" />
<!-- Allow Visual Studio up-to-date check to verify that nothing has changed - https://github.com/dotnet/project-system/blob/main/docs/up-to-date-check.md -->
<UpToDateCheckInput Include="@(InputSpecFiles)"/>
<UpToDateCheckInput Include="@(InputSpecFiles)" />
<OutputSpecFiles Include="@(InputSpecFiles->'%(RelativeDir)%(Filename).generated.cs')" />
</ItemGroup>

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Markdig.Renderers.Roundtrip;
using Markdig.Syntax;
using NUnit.Framework;
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs
{
[TestFixture]
public class TestYamlFrontMatterBlock
{
[TestCase("---\nkey1: value1\nkey2: value2\n---\n\nContent\n")]
[TestCase("No front matter")]
[TestCase("Looks like front matter but actually is not\n---\nkey1: value1\nkey2: value2\n---")]
public void FrontMatterBlockIsPreserved(string value)
{
RoundTrip(value);
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -226,5 +226,27 @@ namespace Markdig.Tests.Specs.Globalization
TestParser.TestSpec("Nutrition |Apple | Oranges\n--|-- | --\nCalories|52|47\nSugar|10g|9g\n\n پێکهاتە |سێو | پڕتەقاڵ\n--|-- | --\nکالۆری|٥٢|٤٧\nشەکر| ١٠گ|٩گ", "<table>\n<thead>\n<tr>\n<th>Nutrition</th>\n<th>Apple</th>\n<th>Oranges</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Calories</td>\n<td>52</td>\n<td>47</td>\n</tr>\n<tr>\n<td>Sugar</td>\n<td>10g</td>\n<td>9g</td>\n</tr>\n</tbody>\n</table>\n<table dir=\"rtl\" align=\"right\">\n<thead>\n<tr>\n<th>پێکهاتە</th>\n<th>سێو</th>\n<th>پڕتەقاڵ</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>کالۆری</td>\n<td>٥٢</td>\n<td>٤٧</td>\n</tr>\n<tr>\n<td>شەکر</td>\n<td>١٠گ</td>\n<td>٩گ</td>\n</tr>\n</tbody>\n</table>", "globalization+advanced+emojis", context: "Example 3\nSection Extensions / Globalization\n");
}
// But if text starts with LTR characters, no attributes are added.
[Test]
public void ExtensionsGlobalization_Example004()
{
// Example 4
// Section: Extensions / Globalization
//
// The following Markdown:
// Foo میوە
//
// میوە bar
//
// Baz میوە
//
// Should be rendered as:
// <p>Foo میوە</p>
// <p dir="rtl">میوە bar</p>
// <p>Baz میوە</p>
TestParser.TestSpec("Foo میوە\n\nمیوە bar\n\nBaz میوە", "<p>Foo میوە</p>\n<p dir=\"rtl\">میوە bar</p>\n<p>Baz میوە</p>", "globalization+advanced+emojis", context: "Example 4\nSection Extensions / Globalization\n");
}
}
}

View File

@@ -186,4 +186,18 @@ Sugar|10g|9g
</tr>
</tbody>
</table>
````````````````````````````````
But if text starts with LTR characters, no attributes are added.
```````````````````````````````` example
Foo میوە
میوە bar
Baz میوە
.
<p>Foo میوە</p>
<p dir="rtl">میوە bar</p>
<p>Baz میوە</p>
````````````````````````````````

View File

@@ -88,5 +88,102 @@ namespace Markdig.Tests
Assert.Throws<ArgumentException>(() => container[0] = two); // two already has a parent
}
[Test]
public void Contains()
{
var container = new MockContainerBlock();
var block = new ParagraphBlock();
Assert.False(container.Contains(block));
container.Add(block);
Assert.True(container.Contains(block));
container.Add(new ParagraphBlock());
Assert.True(container.Contains(block));
container.Insert(0, new ParagraphBlock());
Assert.True(container.Contains(block));
}
[Test]
public void Remove()
{
var container = new MockContainerBlock();
var block = new ParagraphBlock();
Assert.False(container.Remove(block));
Assert.AreEqual(0, container.Count);
Assert.Throws<ArgumentOutOfRangeException>(() => container.RemoveAt(0));
Assert.AreEqual(0, container.Count);
container.Add(block);
Assert.AreEqual(1, container.Count);
Assert.True(container.Remove(block));
Assert.AreEqual(0, container.Count);
Assert.False(container.Remove(block));
Assert.AreEqual(0, container.Count);
container.Add(block);
Assert.AreEqual(1, container.Count);
container.RemoveAt(0);
Assert.AreEqual(0, container.Count);
Assert.Throws<ArgumentOutOfRangeException>(() => container.RemoveAt(0));
Assert.AreEqual(0, container.Count);
container.Add(new ParagraphBlock { Column = 1 });
container.Add(new ParagraphBlock { Column = 2 });
container.Add(new ParagraphBlock { Column = 3 });
container.Add(new ParagraphBlock { Column = 4 });
Assert.AreEqual(4, container.Count);
container.RemoveAt(2);
Assert.AreEqual(3, container.Count);
Assert.AreEqual(4, container[2].Column);
Assert.True(container.Remove(container[1]));
Assert.AreEqual(2, container.Count);
Assert.AreEqual(1, container[0].Column);
Assert.AreEqual(4, container[1].Column);
Assert.Throws<IndexOutOfRangeException>(() => _ = container[2]);
}
[Test]
public void CopyTo()
{
var container = new MockContainerBlock();
var destination = new Block[4];
container.CopyTo(destination, 0);
container.CopyTo(destination, 1);
container.CopyTo(destination, -1);
container.CopyTo(destination, 5);
Assert.Null(destination[0]);
container.Add(new ParagraphBlock());
container.CopyTo(destination, 0);
Assert.NotNull(destination[0]);
Assert.Null(destination[1]);
Assert.Null(destination[2]);
Assert.Null(destination[3]);
container.CopyTo(destination, 2);
Assert.NotNull(destination[0]);
Assert.Null(destination[1]);
Assert.NotNull(destination[2]);
Assert.Null(destination[3]);
Array.Clear(destination);
container.Add(new ParagraphBlock());
container.CopyTo(destination, 1);
Assert.Null(destination[0]);
Assert.NotNull(destination[1]);
Assert.NotNull(destination[2]);
Assert.Null(destination[3]);
Assert.Throws<IndexOutOfRangeException>(() => container.CopyTo(destination, 3));
}
}
}

View File

@@ -0,0 +1,67 @@
using Markdig.Helpers;
using NUnit.Framework;
namespace Markdig.Tests
{
public class TestLazySubstring
{
[Theory]
[TestCase("")]
[TestCase("a")]
[TestCase("foo")]
public void LazySubstring_ReturnsCorrectSubstring(string text)
{
var substring = new LazySubstring(text);
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(text.Length, substring.Length);
Assert.AreEqual(text, substring.AsSpan().ToString());
Assert.AreEqual(text, substring.AsSpan().ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(text.Length, substring.Length);
Assert.AreSame(substring.ToString(), substring.ToString());
Assert.AreEqual(text, substring.ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(text.Length, substring.Length);
Assert.AreEqual(text, substring.AsSpan().ToString());
Assert.AreEqual(text, substring.AsSpan().ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(text.Length, substring.Length);
}
[Theory]
[TestCase("", 0, 0)]
[TestCase("a", 0, 0)]
[TestCase("a", 1, 0)]
[TestCase("a", 0, 1)]
[TestCase("foo", 1, 0)]
[TestCase("foo", 1, 1)]
[TestCase("foo", 1, 2)]
[TestCase("foo", 0, 3)]
public void LazySubstring_ReturnsCorrectSubstring(string text, int start, int length)
{
var substring = new LazySubstring(text, start, length);
Assert.AreEqual(start, substring.Offset);
Assert.AreEqual(length, substring.Length);
string expectedSubstring = text.Substring(start, length);
Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString());
Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString());
Assert.AreEqual(start, substring.Offset);
Assert.AreEqual(length, substring.Length);
Assert.AreSame(substring.ToString(), substring.ToString());
Assert.AreEqual(expectedSubstring, substring.ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(length, substring.Length);
Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString());
Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(length, substring.Length);
}
}
}

View File

@@ -74,6 +74,35 @@ namespace Markdig.Tests
}
}
[Test]
public void TestDocumentToHtmlWithWriter()
{
var writer = new StringWriter();
for (int i = 0; i < 5; i++)
{
MarkdownDocument document = Markdown.Parse("This is a text with some *emphasis*");
document.ToHtml(writer);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with some <em>emphasis</em></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
writer = new StringWriter();
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
for (int i = 0; i < 5; i++)
{
MarkdownDocument document = Markdown.Parse("This is a text with a https://link.tld/", pipeline);
document.ToHtml(writer, pipeline);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with a <a href=\"https://link.tld/\">https://link.tld/</a></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
}
[Test]
public void TestConvert()
{

View File

@@ -1,3 +1,6 @@
using System.Linq;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using NUnit.Framework;
namespace Markdig.Tests
@@ -8,10 +11,21 @@ namespace Markdig.Tests
[TestCase("a \nb", "<p>a<br />\nb</p>\n")]
[TestCase("a\\\nb", "<p>a<br />\nb</p>\n")]
[TestCase("a `b\nc`", "<p>a <code>b c</code></p>\n")]
[TestCase("# Text A\nText B\n\n## Text C", "<h1>Text A</h1>\n<p>Text B</p>\n<h2>Text C</h2>\n")]
public void Test(string value, string expectedHtml)
{
Assert.AreEqual(expectedHtml, Markdown.ToHtml(value));
Assert.AreEqual(expectedHtml, Markdown.ToHtml(value.Replace("\n", "\r\n")));
}
[Test()]
public void TestEscapeLineBreak()
{
var input = "test\\\r\ntest1\r\n";
var doc = Markdown.Parse(input);
var inlines = doc.Descendants<LineBreakInline>().ToList();
Assert.AreEqual(1, inlines.Count, "Invalid number of LineBreakInline");
Assert.True(inlines[0].IsBackslash);
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Markdig.Tests
[TestCase(/* markdownText: */ "# foo\nbar", /* expected: */ "foo\nbar\n")]
[TestCase(/* markdownText: */ "> foo", /* expected: */ "foo\n")]
[TestCase(/* markdownText: */ "> foo\nbar\n> baz", /* expected: */ "foo\nbar\nbaz\n")]
[TestCase(/* markdownText: */ "`foo`", /* expected: */ "foo\n")]
[TestCase(/* markdownText: */ "`foo`", /* expected: */ "foo\n")]
[TestCase(/* markdownText: */ "`foo\nbar`", /* expected: */ "foo bar\n")] // new line within codespan is treated as whitespace (Example317)
[TestCase(/* markdownText: */ "```\nfoo bar\n```", /* expected: */ "foo bar\n")]
[TestCase(/* markdownText: */ "- foo\n- bar\n- baz", /* expected: */ "foo\nbar\nbaz\n")]
@@ -23,7 +23,7 @@ namespace Markdig.Tests
[TestCase(/* markdownText: */ "- foo&lt;baz", /* expected: */ "foo<baz\n")]
[TestCase(/* markdownText: */ "## foo `bar::baz >`", /* expected: */ "foo bar::baz >\n")]
public void TestPlainEnsureNewLine(string markdownText, string expected)
{
{
var actual = Markdown.ToPlainText(markdownText);
Assert.AreEqual(expected, actual);
}
@@ -31,6 +31,7 @@ namespace Markdig.Tests
[Test]
[TestCase(/* markdownText: */ ":::\nfoo\n:::", /* expected: */ "foo\n", /*extensions*/ "customcontainers|advanced")]
[TestCase(/* markdownText: */ ":::bar\nfoo\n:::", /* expected: */ "foo\n", /*extensions*/ "customcontainers+attributes|advanced")]
[TestCase(/* markdownText: */ "| Header1 | Header2 | Header3 |\n|--|--|--|\nt**es**t|value2|value3", /* expected: */ "Header1 Header2 Header3 test value2 value3","pipetables")]
public void TestPlainWithExtensions(string markdownText, string expected, string extensions)
{
TestParser.TestSpec(markdownText, expected, extensions, plainText: true);

View File

@@ -16,10 +16,12 @@ namespace Markdig.Tests
{
var pipelineBuilder = new MarkdownPipelineBuilder();
pipelineBuilder.EnableTrackTrivia();
pipelineBuilder.UseYamlFrontMatter();
MarkdownPipeline pipeline = pipelineBuilder.Build();
MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline);
var sw = new StringWriter();
var nr = new RoundtripRenderer(sw);
pipeline.Setup(nr);
nr.Write(markdownDocument);

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Markdig.Extensions.Yaml;
using Markdig.Renderers;
using Markdig.Syntax;
using NUnit.Framework;
namespace Markdig.Tests
{
public class TestYamlFrontMatterExtension
{
[TestCaseSource(nameof(TestCases))]
public void ProperYamlFrontMatterRenderersAdded(IMarkdownObjectRenderer[] objectRenderers, bool hasYamlFrontMatterHtmlRenderer, bool hasYamlFrontMatterRoundtripRenderer)
{
var builder = new MarkdownPipelineBuilder();
builder.Extensions.Add(new YamlFrontMatterExtension());
var markdownRenderer = new DummyRenderer();
markdownRenderer.ObjectRenderers.AddRange(objectRenderers);
builder.Build().Setup(markdownRenderer);
Assert.That(markdownRenderer.ObjectRenderers.Contains<YamlFrontMatterHtmlRenderer>(), Is.EqualTo(hasYamlFrontMatterHtmlRenderer));
Assert.That(markdownRenderer.ObjectRenderers.Contains<YamlFrontMatterRoundtripRenderer>(), Is.EqualTo(hasYamlFrontMatterRoundtripRenderer));
}
private static IEnumerable<TestCaseData> TestCases()
{
yield return new TestCaseData(new IMarkdownObjectRenderer[]
{
}, false, false) {TestName = "No ObjectRenderers"};
yield return new TestCaseData(new IMarkdownObjectRenderer[]
{
new Markdig.Renderers.Html.CodeBlockRenderer()
}, true, false) {TestName = "Html CodeBlock"};
yield return new TestCaseData(new IMarkdownObjectRenderer[]
{
new Markdig.Renderers.Roundtrip.CodeBlockRenderer()
}, false, true) {TestName = "Roundtrip CodeBlock"};
yield return new TestCaseData(new IMarkdownObjectRenderer[]
{
new Markdig.Renderers.Html.CodeBlockRenderer(),
new Markdig.Renderers.Roundtrip.CodeBlockRenderer()
}, true, true) {TestName = "Html/Roundtrip CodeBlock"};
yield return new TestCaseData(new IMarkdownObjectRenderer[]
{
new Markdig.Renderers.Html.CodeBlockRenderer(),
new Markdig.Renderers.Roundtrip.CodeBlockRenderer(),
new YamlFrontMatterHtmlRenderer()
}, true, true) {TestName = "Html/Roundtrip CodeBlock, Yaml Html"};
yield return new TestCaseData(new IMarkdownObjectRenderer[]
{
new Markdig.Renderers.Html.CodeBlockRenderer(),
new Markdig.Renderers.Roundtrip.CodeBlockRenderer(),
new YamlFrontMatterRoundtripRenderer()
}, true, true) { TestName = "Html/Roundtrip CodeBlock, Yaml Roundtrip" };
}
private class DummyRenderer : IMarkdownRenderer
{
public DummyRenderer()
{
ObjectRenderers = new ObjectRendererCollection();
}
public event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteBefore;
public event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteAfter;
public ObjectRendererCollection ObjectRenderers { get; }
public object Render(MarkdownObject markdownObject)
{
return null;
}
}
}
}

View File

@@ -49,6 +49,53 @@ namespace Markdig.Extensions.AutoLinks
return false;
}
var startPosition = slice.Start;
int domainOffset = 0;
var c = slice.CurrentChar;
// Precheck URL
switch (c)
{
case 'h':
if (slice.MatchLowercase("ttp://", 1))
{
domainOffset = 7; // http://
}
else if (slice.MatchLowercase("ttps://", 1))
{
domainOffset = 8; // https://
}
else return false;
break;
case 'f':
if (!slice.MatchLowercase("tp://", 1))
{
return false;
}
domainOffset = 6; // ftp://
break;
case 'm':
if (!slice.MatchLowercase("ailto:", 1))
{
return false;
}
break;
case 't':
if (!slice.MatchLowercase("el:", 1))
{
return false;
}
domainOffset = 4;
break;
case 'w':
if (!slice.MatchLowercase("ww.", 1)) // We won't match http:/www. or /www.xxx
{
return false;
}
domainOffset = 4; // www.
break;
}
List<char> pendingEmphasis = _listOfCharCache.Get();
try
{
@@ -58,53 +105,6 @@ namespace Markdig.Extensions.AutoLinks
return false;
}
var startPosition = slice.Start;
int domainOffset = 0;
var c = slice.CurrentChar;
// Precheck URL
switch (c)
{
case 'h':
if (slice.MatchLowercase("ttp://", 1))
{
domainOffset = 7; // http://
}
else if (slice.MatchLowercase("ttps://", 1))
{
domainOffset = 8; // https://
}
else return false;
break;
case 'f':
if (!slice.MatchLowercase("tp://", 1))
{
return false;
}
domainOffset = 6; // ftp://
break;
case 'm':
if (!slice.MatchLowercase("ailto:", 1))
{
return false;
}
break;
case 't':
if (!slice.MatchLowercase("el:", 1))
{
return false;
}
domainOffset = 4;
break;
case 'w':
if (!slice.MatchLowercase("ww.", 1)) // We won't match http:/www. or /www.xxx
{
return false;
}
domainOffset = 4; // www.
break;
}
// Parse URL
if (!LinkHelper.TryParseUrl(ref slice, out string? link, out _, true))
{

View File

@@ -28,9 +28,16 @@ namespace Markdig.Extensions.Bootstrap
private static void PipelineOnDocumentProcessed(MarkdownDocument document)
{
foreach(var node in document.Descendants())
foreach (var node in document.Descendants())
{
if (node is Block)
if (node.IsInline)
{
if (node.IsContainerInline && node is LinkInline link && link.IsImage)
{
link.GetAttributes().AddClass("img-fluid");
}
}
else if (node.IsContainerBlock)
{
if (node is Tables.Table)
{
@@ -44,18 +51,14 @@ namespace Markdig.Extensions.Bootstrap
{
node.GetAttributes().AddClass("figure");
}
else if (node is Figures.FigureCaption)
}
else
{
if (node is Figures.FigureCaption)
{
node.GetAttributes().AddClass("figure-caption");
}
}
else if (node is Inline)
{
if (node is LinkInline link && link.IsImage)
{
link.GetAttributes().AddClass("img-fluid");
}
}
}
}
}

View File

@@ -93,20 +93,23 @@ namespace Markdig.Extensions.Globalization
{
for (int i = slice.Start; i <= slice.End; i++)
{
if (slice[i] < 128)
char c = slice[i];
if (c < 128)
{
if (CharHelper.IsAlpha(c))
{
return false;
}
continue;
}
int rune;
if (CharHelper.IsHighSurrogate(slice[i]) && i < slice.End && CharHelper.IsLowSurrogate(slice[i + 1]))
int rune = c;
if (CharHelper.IsHighSurrogate(c) && i < slice.End && CharHelper.IsLowSurrogate(slice[i + 1]))
{
Debug.Assert(char.IsSurrogatePair(slice[i], slice[i + 1]));
rune = char.ConvertToUtf32(slice[i], slice[i + 1]);
}
else
{
rune = slice[i];
Debug.Assert(char.IsSurrogatePair(c, slice[i + 1]));
rune = char.ConvertToUtf32(c, slice[i + 1]);
i++;
}
if (CharHelper.IsRightToLeft(rune))

View File

@@ -17,122 +17,154 @@ namespace Markdig.Extensions.Tables
{
protected override void Write(HtmlRenderer renderer, Table table)
{
renderer.EnsureLine();
renderer.Write("<table").WriteAttributes(table).WriteLine('>');
bool hasBody = false;
bool hasAlreadyHeader = false;
bool isHeaderOpen = false;
bool hasColumnWidth = false;
foreach (var tableColumnDefinition in table.ColumnDefinitions)
if (renderer.EnableHtmlForBlock)
{
if (tableColumnDefinition.Width != 0.0f && tableColumnDefinition.Width != 1.0f)
{
hasColumnWidth = true;
break;
}
}
renderer.EnsureLine();
renderer.Write("<table").WriteAttributes(table).WriteLine('>');
if (hasColumnWidth)
{
bool hasBody = false;
bool hasAlreadyHeader = false;
bool isHeaderOpen = false;
bool hasColumnWidth = false;
foreach (var tableColumnDefinition in table.ColumnDefinitions)
{
var width = Math.Round(tableColumnDefinition.Width*100)/100;
var widthValue = string.Format(CultureInfo.InvariantCulture, "{0:0.##}", width);
renderer.WriteLine($"<col style=\"width:{widthValue}%\" />");
if (tableColumnDefinition.Width != 0.0f && tableColumnDefinition.Width != 1.0f)
{
hasColumnWidth = true;
break;
}
}
}
foreach (var rowObj in table)
{
var row = (TableRow)rowObj;
if (row.IsHeader)
if (hasColumnWidth)
{
// Allow a single thead
if (!hasAlreadyHeader)
foreach (var tableColumnDefinition in table.ColumnDefinitions)
{
renderer.WriteLine("<thead>");
isHeaderOpen = true;
var width = Math.Round(tableColumnDefinition.Width * 100) / 100;
var widthValue = string.Format(CultureInfo.InvariantCulture, "{0:0.##}", width);
renderer.WriteLine($"<col style=\"width:{widthValue}%\" />");
}
hasAlreadyHeader = true;
}
else if (!hasBody)
{
if (isHeaderOpen)
{
renderer.WriteLine("</thead>");
isHeaderOpen = false;
}
renderer.WriteLine("<tbody>");
hasBody = true;
}
renderer.Write("<tr").WriteAttributes(row).WriteLine('>');
for (int i = 0; i < row.Count; i++)
foreach (var rowObj in table)
{
var cellObj = row[i];
var cell = (TableCell)cellObj;
renderer.EnsureLine();
renderer.Write(row.IsHeader ? "<th" : "<td");
if (cell.ColumnSpan != 1)
var row = (TableRow)rowObj;
if (row.IsHeader)
{
renderer.Write($" colspan=\"{cell.ColumnSpan}\"");
}
if (cell.RowSpan != 1)
{
renderer.Write($" rowspan=\"{cell.RowSpan}\"");
}
if (table.ColumnDefinitions.Count > 0)
{
var columnIndex = cell.ColumnIndex < 0 || cell.ColumnIndex >= table.ColumnDefinitions.Count
? i
: cell.ColumnIndex;
columnIndex = columnIndex >= table.ColumnDefinitions.Count ? table.ColumnDefinitions.Count - 1 : columnIndex;
var alignment = table.ColumnDefinitions[columnIndex].Alignment;
if (alignment.HasValue)
// Allow a single thead
if (!hasAlreadyHeader)
{
switch (alignment)
renderer.WriteLine("<thead>");
isHeaderOpen = true;
}
hasAlreadyHeader = true;
}
else if (!hasBody)
{
if (isHeaderOpen)
{
renderer.WriteLine("</thead>");
isHeaderOpen = false;
}
renderer.WriteLine("<tbody>");
hasBody = true;
}
renderer.Write("<tr").WriteAttributes(row).WriteLine('>');
for (int i = 0; i < row.Count; i++)
{
var cellObj = row[i];
var cell = (TableCell)cellObj;
renderer.EnsureLine();
renderer.Write(row.IsHeader ? "<th" : "<td");
if (cell.ColumnSpan != 1)
{
renderer.Write($" colspan=\"{cell.ColumnSpan}\"");
}
if (cell.RowSpan != 1)
{
renderer.Write($" rowspan=\"{cell.RowSpan}\"");
}
if (table.ColumnDefinitions.Count > 0)
{
var columnIndex = cell.ColumnIndex < 0 || cell.ColumnIndex >= table.ColumnDefinitions.Count
? i
: cell.ColumnIndex;
columnIndex = columnIndex >= table.ColumnDefinitions.Count ? table.ColumnDefinitions.Count - 1 : columnIndex;
var alignment = table.ColumnDefinitions[columnIndex].Alignment;
if (alignment.HasValue)
{
case TableColumnAlign.Center:
renderer.Write(" style=\"text-align: center;\"");
break;
case TableColumnAlign.Right:
renderer.Write(" style=\"text-align: right;\"");
break;
case TableColumnAlign.Left:
renderer.Write(" style=\"text-align: left;\"");
break;
switch (alignment)
{
case TableColumnAlign.Center:
renderer.Write(" style=\"text-align: center;\"");
break;
case TableColumnAlign.Right:
renderer.Write(" style=\"text-align: right;\"");
break;
case TableColumnAlign.Left:
renderer.Write(" style=\"text-align: left;\"");
break;
}
}
}
}
renderer.WriteAttributes(cell);
renderer.Write('>');
var previousImplicitParagraph = renderer.ImplicitParagraph;
if (cell.Count == 1)
{
renderer.ImplicitParagraph = true;
}
renderer.Write(cell);
renderer.ImplicitParagraph = previousImplicitParagraph;
renderer.WriteAttributes(cell);
renderer.Write('>');
renderer.WriteLine(row.IsHeader ? "</th>" : "</td>");
var previousImplicitParagraph = renderer.ImplicitParagraph;
if (cell.Count == 1)
{
renderer.ImplicitParagraph = true;
}
renderer.Write(cell);
renderer.ImplicitParagraph = previousImplicitParagraph;
renderer.WriteLine(row.IsHeader ? "</th>" : "</td>");
}
renderer.WriteLine("</tr>");
}
renderer.WriteLine("</tr>");
}
if (hasBody)
{
renderer.WriteLine("</tbody>");
if (hasBody)
{
renderer.WriteLine("</tbody>");
}
else if (isHeaderOpen)
{
renderer.WriteLine("</thead>");
}
renderer.WriteLine("</table>");
}
else if (isHeaderOpen)
else
{
renderer.WriteLine("</thead>");
//no html, just write the table contents
var impliciParagraph = renderer.ImplicitParagraph;
//enable implicit paragraphs to avoid newlines after each cell
renderer.ImplicitParagraph = true;
foreach (var rowObj in table)
{
var row = (TableRow)rowObj;
for (int i = 0; i < row.Count; i++)
{
var cellObj = row[i];
var cell = (TableCell)cellObj;
renderer.Write(cell);
//write a space after each cell to avoid text being merged with the next cell
renderer.Write(' ');
}
}
renderer.ImplicitParagraph = impliciParagraph;
}
renderer.WriteLine("</table>");
}
}
}

View File

@@ -1,10 +1,9 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// 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.Renderers;
using Markdig.Renderers.Html;
namespace Markdig.Extensions.Yaml
{
@@ -24,9 +23,14 @@ namespace Markdig.Extensions.Yaml
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (!renderer.ObjectRenderers.Contains<YamlFrontMatterRenderer>())
if (!renderer.ObjectRenderers.Contains<YamlFrontMatterHtmlRenderer>())
{
renderer.ObjectRenderers.InsertBefore<CodeBlockRenderer>(new YamlFrontMatterRenderer());
renderer.ObjectRenderers.InsertBefore<Renderers.Html.CodeBlockRenderer>(new YamlFrontMatterHtmlRenderer());
}
if (!renderer.ObjectRenderers.Contains<YamlFrontMatterRoundtripRenderer>())
{
renderer.ObjectRenderers.InsertBefore<Renderers.Roundtrip.CodeBlockRenderer>(new YamlFrontMatterRoundtripRenderer());
}
}
}

View File

@@ -11,7 +11,7 @@ namespace Markdig.Extensions.Yaml
/// Empty renderer for a <see cref="YamlFrontMatterBlock"/>
/// </summary>
/// <seealso cref="HtmlObjectRenderer{YamlFrontMatterBlock}" />
public class YamlFrontMatterRenderer : HtmlObjectRenderer<YamlFrontMatterBlock>
public class YamlFrontMatterHtmlRenderer : HtmlObjectRenderer<YamlFrontMatterBlock>
{
protected override void Write(HtmlRenderer renderer, YamlFrontMatterBlock obj)
{

View File

@@ -0,0 +1,27 @@
// 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 System;
using Markdig.Renderers;
using Markdig.Renderers.Roundtrip;
namespace Markdig.Extensions.Yaml
{
public class YamlFrontMatterRoundtripRenderer : MarkdownObjectRenderer<RoundtripRenderer, YamlFrontMatterBlock>
{
private readonly CodeBlockRenderer _codeBlockRenderer;
public YamlFrontMatterRoundtripRenderer()
{
_codeBlockRenderer = new CodeBlockRenderer();
}
protected override void Write(RoundtripRenderer renderer, YamlFrontMatterBlock obj)
{
renderer.Writer.WriteLine("---");
_codeBlockRenderer.Write(renderer, obj);
renderer.Writer.WriteLine("---");
}
}
}

View File

@@ -1,6 +1,7 @@
// 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.
namespace Markdig.Helpers
{
/// <summary>

View File

@@ -0,0 +1,30 @@
// 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.Syntax;
using System;
namespace Markdig.Helpers
{
// Used to avoid the overhead of type covariance checks
internal readonly struct BlockWrapper : IEquatable<BlockWrapper>
{
public readonly Block Block;
public BlockWrapper(Block block)
{
Block = block;
}
public static implicit operator Block(BlockWrapper wrapper) => wrapper.Block;
public static implicit operator BlockWrapper(Block block) => new BlockWrapper(block);
public bool Equals(BlockWrapper other) => ReferenceEquals(Block, other.Block);
public override bool Equals(object obj) => Block.Equals(obj);
public override int GetHashCode() => Block.GetHashCode();
}
}

View File

@@ -744,7 +744,7 @@ namespace Markdig.Helpers
return cache[number];
}
return number.ToString();
return number.ToString(CultureInfo.InvariantCulture);
}
}
}

View File

@@ -7,6 +7,11 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
#if NETCOREAPP3_1_OR_GREATER
using System.Numerics;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
#endif
namespace Markdig.Helpers
{
@@ -16,6 +21,10 @@ namespace Markdig.Helpers
/// <typeparam name="T"></typeparam>
public sealed class CharacterMap<T> where T : class
{
#if NETCOREAPP3_1_OR_GREATER
private readonly Vector128<byte> _asciiBitmap;
#endif
private readonly T[] asciiMap;
private readonly Dictionary<uint, T>? nonAsciiMap;
private readonly BoolVector128 isOpeningCharacter;
@@ -39,6 +48,11 @@ namespace Markdig.Helpers
if (openingChar < 128)
{
maxChar = Math.Max(maxChar, openingChar);
if (openingChar == 0)
{
ThrowHelper.ArgumentOutOfRangeException("Null is not a valid opening character.", nameof(maps));
}
}
else
{
@@ -64,6 +78,23 @@ namespace Markdig.Helpers
nonAsciiMap[openingChar] = state.Value;
}
}
#if NETCOREAPP3_1_OR_GREATER
if (nonAsciiMap is null)
{
long bitmap_0_3 = 0;
long bitmap_4_7 = 0;
foreach (char openingChar in OpeningCharacters)
{
int position = (openingChar >> 4) | ((openingChar & 0x0F) << 3);
if (position < 64) bitmap_0_3 |= 1L << position;
else bitmap_4_7 |= 1L << (position - 64);
}
_asciiBitmap = Vector128.Create(bitmap_0_3, bitmap_4_7).AsByte();
}
#endif
}
/// <summary>
@@ -105,9 +136,63 @@ namespace Markdig.Helpers
/// <returns>Index position within the string of the first opening character found in the specified text; if not found, returns -1</returns>
public int IndexOfOpeningCharacter(string text, int start, int end)
{
Debug.Assert(text is not null);
Debug.Assert(start >= 0 && end >= 0);
Debug.Assert(end - start + 1 >= 0);
Debug.Assert(end - start + 1 <= text.Length);
if (nonAsciiMap is null)
{
#if NETCOREAPP3_1_OR_GREATER
if (Ssse3.IsSupported && BitConverter.IsLittleEndian)
{
// Based on http://0x80.pl/articles/simd-byte-lookup.html#universal-algorithm
// Optimized for sets in the [1, 127] range
int lengthMinusOne = end - start;
int charsToProcessVectorized = lengthMinusOne & ~(2 * Vector128<short>.Count - 1);
int finalStart = start + charsToProcessVectorized;
if (start < finalStart)
{
ref char textStartRef = ref Unsafe.Add(ref Unsafe.AsRef(in text.GetPinnableReference()), start);
Vector128<byte> bitmap = _asciiBitmap;
do
{
// Load 32 bytes (16 chars) into two Vector128<short>s (chars)
// Drop the high byte of each char
// Pack the remaining bytes into a single Vector128<byte>
Vector128<byte> input = Sse2.PackUnsignedSaturate(
Unsafe.ReadUnaligned<Vector128<short>>(ref Unsafe.As<char, byte>(ref textStartRef)),
Unsafe.ReadUnaligned<Vector128<short>>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref textStartRef, Vector128<short>.Count))));
// Extract the higher nibble of each character ((input >> 4) & 0xF)
Vector128<byte> higherNibbles = Sse2.And(Sse2.ShiftRightLogical(input.AsUInt16(), 4).AsByte(), Vector128.Create((byte)0xF));
// Lookup the matching higher nibble for each character based on the lower nibble
// PSHUFB will set the result to 0 for any non-ASCII (> 127) character
Vector128<byte> bitsets = Ssse3.Shuffle(bitmap, input);
// Calculate a bitmask (1 << (higherNibble % 8)) for each character
Vector128<byte> bitmask = Ssse3.Shuffle(Vector128.Create(0x8040201008040201).AsByte(), higherNibbles);
// Check which characters are present in the set
// We are relying on bitsets being zero for non-ASCII characters
Vector128<byte> result = Sse2.And(bitsets, bitmask);
if (!result.Equals(Vector128<byte>.Zero))
{
int resultMask = ~Sse2.MoveMask(Sse2.CompareEqual(result, Vector128<byte>.Zero));
return start + BitOperations.TrailingZeroCount((uint)resultMask);
}
start += 2 * Vector128<short>.Count;
textStartRef = ref Unsafe.Add(ref textStartRef, 2 * Vector128<short>.Count);
}
while (start != finalStart);
}
}
ref char textRef = ref Unsafe.AsRef(in text.GetPinnableReference());
for (; start <= end; start++)
{

View File

@@ -0,0 +1,44 @@
// 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 System;
using System.Diagnostics;
namespace Markdig.Helpers
{
internal struct LazySubstring
{
private string _text;
public int Offset;
public int Length;
public LazySubstring(string text)
{
_text = text;
Offset = 0;
Length = text.Length;
}
public LazySubstring(string text, int offset, int length)
{
Debug.Assert((ulong)offset + (ulong)length <= (ulong)text.Length, $"{offset}-{length} in {text}");
_text = text;
Offset = offset;
Length = length;
}
public ReadOnlySpan<char> AsSpan() => _text.AsSpan(Offset, Length);
public override string ToString()
{
if (Offset != 0 || Length != _text.Length)
{
_text = _text.Substring(Offset, Length);
Offset = 0;
}
return _text;
}
}
}

View File

@@ -3,7 +3,7 @@
// See the license.txt file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
namespace Markdig.Helpers
{
@@ -13,14 +13,14 @@ namespace Markdig.Helpers
/// <typeparam name="T">Type of the object to cache</typeparam>
public abstract class ObjectCache<T> where T : class
{
private readonly Stack<T> builders;
private readonly ConcurrentQueue<T> _builders;
/// <summary>
/// Initializes a new instance of the <see cref="ObjectCache{T}"/> class.
/// </summary>
protected ObjectCache()
{
builders = new Stack<T>(4);
_builders = new ConcurrentQueue<T>();
}
/// <summary>
@@ -28,10 +28,7 @@ namespace Markdig.Helpers
/// </summary>
public void Clear()
{
lock (builders)
{
builders.Clear();
}
_builders.Clear();
}
/// <summary>
@@ -40,12 +37,9 @@ namespace Markdig.Helpers
/// <returns></returns>
public T Get()
{
lock (builders)
if (_builders.TryDequeue(out T instance))
{
if (builders.Count > 0)
{
return builders.Pop();
}
return instance;
}
return NewInstance();
@@ -60,10 +54,7 @@ namespace Markdig.Helpers
{
if (instance is null) ThrowHelper.ArgumentNullException(nameof(instance));
Reset(instance);
lock (builders)
{
builders.Push(instance);
}
_builders.Enqueue(instance);
}
/// <summary>

View File

@@ -6,7 +6,6 @@ using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Markdig.Extensions.SelfPipeline;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Renderers;
@@ -41,11 +40,11 @@ namespace Markdig
return DefaultPipeline;
}
var selfPipeline = pipeline.Extensions.Find<SelfPipelineExtension>();
if (selfPipeline is not null)
if (pipeline.SelfPipeline is not null)
{
return selfPipeline.CreatePipelineFromInput(markdown);
return pipeline.SelfPipeline.CreatePipelineFromInput(markdown);
}
return pipeline;
}
@@ -132,6 +131,28 @@ namespace Markdig
return renderer.Writer.ToString() ?? string.Empty;
}
/// <summary>
/// Converts a Markdown document to HTML.
/// </summary>
/// <param name="document">A Markdown document.</param>
/// <param name="writer">The destination <see cref="TextWriter"/> that will receive the result of the conversion.</param>
/// <param name="pipeline">The pipeline used for the conversion.</param>
/// <returns>The result of the conversion</returns>
/// <exception cref="ArgumentNullException">if markdown document variable is null</exception>
public static void ToHtml(this MarkdownDocument document, TextWriter writer, MarkdownPipeline? pipeline = null)
{
if (document is null) ThrowHelper.ArgumentNullException(nameof(document));
if (writer is null) ThrowHelper.ArgumentNullException_writer();
pipeline ??= DefaultPipeline;
using var rentedRenderer = pipeline.RentHtmlRenderer(writer);
HtmlRenderer renderer = rentedRenderer.Instance;
renderer.Render(document);
renderer.Writer.Flush();
}
/// <summary>
/// Converts a Markdown string to HTML and output to the specified writer.
/// </summary>

View File

@@ -4,7 +4,7 @@
using System;
using System.IO;
using System.Text;
using Markdig.Extensions.SelfPipeline;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Renderers;
@@ -13,11 +13,10 @@ namespace Markdig
{
/// <summary>
/// This class is the Markdown pipeline build from a <see cref="MarkdownPipelineBuilder"/>.
/// <para>An instance of <see cref="MarkdownPipeline"/> is immutable, thread-safe, and should be reused when parsing multiple inputs.</para>
/// </summary>
public sealed class MarkdownPipeline
{
// This class is immutable
/// <summary>
/// Initializes a new instance of the <see cref="MarkdownPipeline" /> class.
/// </summary>
@@ -36,6 +35,8 @@ namespace Markdig
InlineParsers = inlineParsers;
DebugLog = debugLog;
DocumentProcessed = documentProcessed;
SelfPipeline = Extensions.Find<SelfPipelineExtension>();
}
internal bool PreciseSourceLocation { get; set; }
@@ -54,6 +55,8 @@ namespace Markdig
internal ProcessDocumentDelegate? DocumentProcessed;
internal SelfPipelineExtension? SelfPipeline;
/// <summary>
/// True to parse trivia such as whitespace, extra heading characters and unescaped
/// string values.
@@ -94,7 +97,7 @@ namespace Markdig
internal sealed class HtmlRendererCache : ObjectCache<HtmlRenderer>
{
private static readonly TextWriter s_dummyWriter = new StringWriter();
private static readonly TextWriter s_dummyWriter = new FastStringWriter();
private readonly MarkdownPipeline _pipeline;
private readonly bool _customWriter;

View File

@@ -75,7 +75,15 @@ namespace Markdig.Parsers
/// <summary>
/// Gets the next block in a <see cref="BlockParser.TryContinue"/>.
/// </summary>
public Block? NextContinue => currentStackIndex + 1 < OpenedBlocks.Count ? OpenedBlocks[currentStackIndex + 1] : null;
public Block? NextContinue
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
int index = currentStackIndex + 1;
return index < OpenedBlocks.Count ? OpenedBlocks[index].Block : null;
}
}
/// <summary>
/// Gets the root document.
@@ -145,7 +153,7 @@ namespace Markdig.Parsers
/// <summary>
/// Gets the current stack of <see cref="Block"/> being processed.
/// </summary>
private List<Block> OpenedBlocks { get; } = new();
private List<BlockWrapper> OpenedBlocks { get; } = new();
private bool ContinueProcessingLine { get; set; }
@@ -446,7 +454,7 @@ namespace Markdig.Parsers
// If we close a block, we close all blocks above
for (int i = OpenedBlocks.Count - 1; i >= 0; i--)
{
if (OpenedBlocks[i] == block)
if (ReferenceEquals(OpenedBlocks[i].Block, block))
{
for (int j = OpenedBlocks.Count - 1; j >= i; j--)
{
@@ -465,7 +473,7 @@ namespace Markdig.Parsers
{
for (int i = OpenedBlocks.Count - 1; i >= 1; i--)
{
if (OpenedBlocks[i] == block)
if (ReferenceEquals(OpenedBlocks[i].Block, block))
{
block.Parent!.Remove(block);
OpenedBlocks.RemoveAt(i);
@@ -510,7 +518,7 @@ namespace Markdig.Parsers
/// <param name="index">The index.</param>
private void Close(int index)
{
var block = OpenedBlocks[index];
var block = OpenedBlocks[index].Block;
// If the pending object is removed, we need to remove it from the parent container
if (block.Parser != null)
{
@@ -518,9 +526,9 @@ namespace Markdig.Parsers
{
block.Parent?.Remove(block);
if (block is LeafBlock leaf)
if (block.IsLeafBlock)
{
leaf.Lines.Release();
Unsafe.As<LeafBlock>(block).Lines.Release();
}
}
else
@@ -542,7 +550,7 @@ namespace Markdig.Parsers
// Close any previous blocks not opened
for (int i = OpenedBlocks.Count - 1; i >= 1; i--)
{
var block = OpenedBlocks[i];
var block = OpenedBlocks[i].Block;
// Stop on the first open block
if (!force && block.IsOpen)
@@ -583,7 +591,7 @@ namespace Markdig.Parsers
{
for (int i = 1; i < OpenedBlocks.Count; i++)
{
OpenedBlocks[i].IsOpen = true;
OpenedBlocks[i].Block.IsOpen = true;
}
}
@@ -593,13 +601,13 @@ namespace Markdig.Parsers
/// <param name="stackIndex">Index of a block in a stack considered as the last block to update from.</param>
private void UpdateLastBlockAndContainer(int stackIndex = -1)
{
List<Block> openedBlocks = OpenedBlocks;
List<BlockWrapper> openedBlocks = OpenedBlocks;
currentStackIndex = stackIndex < 0 ? openedBlocks.Count - 1 : stackIndex;
Block? currentBlock = null;
for (int i = openedBlocks.Count - 1; i >= 0; i--)
{
var block = openedBlocks[i];
var block = openedBlocks[i].Block;
currentBlock ??= block;
if (block.IsContainerBlock)
@@ -632,13 +640,13 @@ namespace Markdig.Parsers
// They will be marked as open in the following loop
for (int i = 1; i < OpenedBlocks.Count; i++)
{
OpenedBlocks[i].IsOpen = false;
OpenedBlocks[i].Block.IsOpen = false;
}
// Process any current block potentially opened
for (int i = 1; i < OpenedBlocks.Count; i++)
{
var block = OpenedBlocks[i];
var block = OpenedBlocks[i].Block;
ParseIndent();

View File

@@ -159,8 +159,8 @@ namespace Markdig.Parsers
int tagIndex = match.Value;
// Cannot start with </script </pre or </style
if ((tagIndex == 49 || tagIndex == 50 || tagIndex == 53))
// Cannot start with </script </pre or </style or </textArea
if ((tagIndex == 49 || tagIndex == 50 || tagIndex == 53 || tagIndex == 56))
{
if (c == '/' || hasLeadingClose)
{
@@ -241,6 +241,15 @@ namespace Markdig.Parsers
htmlBlock.UpdateSpanEnd(index + "</style>".Length);
result = BlockState.Break;
}
else
{
index = line.IndexOf("</textarea>", 0, true);
if (index >= 0)
{
htmlBlock.UpdateSpanEnd(index + "</textarea>".Length);
result = BlockState.Break;
}
}
}
}
break;
@@ -289,7 +298,7 @@ namespace Markdig.Parsers
return BlockState.Continue;
}
private static readonly CompactPrefixTree<int> HtmlTags = new(65, 93, 82)
private static readonly CompactPrefixTree<int> HtmlTags = new(66, 94, 83)
{
{ "address", 0 },
{ "article", 1 },
@@ -347,15 +356,16 @@ namespace Markdig.Parsers
{ "style", 53 }, // <=== special group 1
{ "summary", 54 },
{ "table", 55 },
{ "tbody", 56 },
{ "td", 57 },
{ "tfoot", 58 },
{ "th", 59 },
{ "thead", 60 },
{ "title", 61 },
{ "tr", 62 },
{ "track", 63 },
{ "ul", 64 }
{ "textarea", 56 }, // <=== special group 1
{ "tbody", 57 },
{ "td", 58 },
{ "tfoot", 59 },
{ "th", 60 },
{ "thead", 61 },
{ "title", 62 },
{ "tr", 63 },
{ "track", 64 },
{ "ul", 65 }
};
}
}

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Markdig.Helpers;
using Markdig.Parsers.Inlines;
using Markdig.Syntax;
@@ -113,11 +114,6 @@ namespace Markdig.Parsers
/// </summary>
public LiteralInlineParser LiteralInlineParser { get; } = new();
public int GetSourcePosition(int sliceOffset)
{
return GetSourcePosition(sliceOffset, out _, out _);
}
public SourceSpan GetSourcePositionFromLocalSpan(SourceSpan span)
{
if (span.IsEmpty)
@@ -125,7 +121,7 @@ namespace Markdig.Parsers
return SourceSpan.Empty;
}
return new SourceSpan(GetSourcePosition(span.Start, out _, out _), GetSourcePosition(span.End, out _, out _));
return new SourceSpan(GetSourcePosition(span.Start), GetSourcePosition(span.End));
}
/// <summary>
@@ -142,17 +138,26 @@ namespace Markdig.Parsers
int position = 0;
if (PreciseSourceLocation)
{
#if NET
var offsets = CollectionsMarshal.AsSpan(lineOffsets);
for (; (uint)lineIndex < (uint)offsets.Length; lineIndex++)
{
ref var lineOffset = ref offsets[lineIndex];
#else
for (; lineIndex < lineOffsets.Count; lineIndex++)
{
var lineOffset = lineOffsets[lineIndex];
#endif
if (sliceOffset <= lineOffset.End)
{
// Use the beginning of the line as a previous slice offset
// (since it is on the same line)
previousSliceOffset = lineOffsets[lineIndex].Start;
previousSliceOffset = lineOffset.Start;
var delta = sliceOffset - previousSliceOffset;
column = lineOffsets[lineIndex].Column + delta;
position = lineOffset.LinePosition + delta + lineOffsets[lineIndex].Offset;
column = lineOffset.Column + delta;
position = lineOffset.LinePosition + delta + lineOffset.Offset;
previousLineIndexForSliceOffset = lineIndex;
// Return an absolute line index
@@ -164,6 +169,41 @@ namespace Markdig.Parsers
return position;
}
/// <summary>
/// Gets the source position for the specified offset within the current slice.
/// </summary>
/// <param name="sliceOffset">The slice offset.</param>
/// <returns>The source position</returns>
public int GetSourcePosition(int sliceOffset)
{
if (PreciseSourceLocation)
{
int lineIndex = sliceOffset >= previousSliceOffset ? previousLineIndexForSliceOffset : 0;
#if NET
var offsets = CollectionsMarshal.AsSpan(lineOffsets);
for (; (uint)lineIndex < (uint)offsets.Length; lineIndex++)
{
ref var lineOffset = ref offsets[lineIndex];
#else
for (; lineIndex < lineOffsets.Count; lineIndex++)
{
var lineOffset = lineOffsets[lineIndex];
#endif
if (sliceOffset <= lineOffset.End)
{
previousLineIndexForSliceOffset = lineIndex;
previousSliceOffset = lineOffset.Start;
return sliceOffset - lineOffset.Start + lineOffset.LinePosition + lineOffset.Offset;
}
}
}
return 0;
}
/// <summary>
/// Processes the inline of the specified <see cref="LeafBlock"/>.
/// </summary>

View File

@@ -6,6 +6,7 @@ using Markdig.Helpers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using System;
using System.Diagnostics;
namespace Markdig.Parsers.Inlines
{
@@ -54,6 +55,7 @@ namespace Markdig.Parsers.Inlines
// whitespace from the opening or closing backtick strings.
bool allSpace = true;
bool containsNewLine = false;
var contentEnd = -1;
while (c != '\0')
@@ -61,10 +63,12 @@ namespace Markdig.Parsers.Inlines
// Transform '\n' into a single space
if (c == '\n')
{
containsNewLine = true;
c = ' ';
}
else if (c == '\r')
{
containsNewLine = true;
slice.SkipChar();
c = slice.CurrentChar;
continue;
@@ -100,14 +104,19 @@ namespace Markdig.Parsers.Inlines
{
ReadOnlySpan<char> contentSpan = builder.AsSpan();
var content = containsNewLine
? new LazySubstring(contentSpan.ToString())
: new LazySubstring(slice.Text, contentStart, contentSpan.Length);
Debug.Assert(contentSpan.SequenceEqual(content.AsSpan()));
// Remove one space from front and back if the string is not all spaces
if (!allSpace && contentSpan.Length > 2 && contentSpan[0] == ' ' && contentSpan[contentSpan.Length - 1] == ' ')
{
contentSpan = contentSpan.Slice(1, contentSpan.Length - 2);
content.Offset++;
content.Length -= 2;
}
string content = contentSpan.ToString();
int delimiterCount = Math.Min(openSticks, closeSticks);
var spanStart = processor.GetSourcePosition(startPosition, out int line, out int column);
var spanEnd = processor.GetSourcePosition(slice.Start - 1);

View File

@@ -116,17 +116,22 @@ namespace Markdig.Parsers.Inlines
break;
}
// If we have a delimiter, we search into it as we should have a tree of EmphasisDelimiterInline
if (child is EmphasisDelimiterInline delimiter)
if (child.IsContainer && child is DelimiterInline delimiterInline)
{
delimiters ??= inlinesCache.Get();
delimiters.Add(delimiter);
child = delimiter.FirstChild;
continue;
}
// If we have a delimiter, we search into it as we should have a tree of EmphasisDelimiterInline
if (delimiterInline is EmphasisDelimiterInline delimiter)
{
delimiters ??= inlinesCache.Get();
delimiters.Add(delimiter);
}
// Follow DelimiterInline (EmphasisDelimiter, TableDelimiter...)
child = child is DelimiterInline delimiterInline ? delimiterInline.FirstChild : child.NextSibling;
// Follow DelimiterInline (EmphasisDelimiter, TableDelimiter...)
child = delimiterInline.FirstChild;
}
else
{
child = child.NextSibling;
}
}
if (delimiters != null)

View File

@@ -41,45 +41,35 @@ namespace Markdig.Parsers.Inlines
}
// A backslash at the end of the line is a [hard line break]:
if (processor.TrackTrivia)
if (c == '\n' || c == '\r')
{
if (c == '\n' || c == '\r')
var newLine = c == '\n' ? NewLine.LineFeed : NewLine.CarriageReturn;
if (c == '\r' && slice.PeekChar() == '\n')
{
var newLine = c == '\n' ? NewLine.LineFeed : NewLine.CarriageReturn;
if (c == '\r' && slice.PeekChar() == '\n')
{
newLine = NewLine.CarriageReturnLineFeed;
}
processor.Inline = new LineBreakInline()
{
IsHard = true,
IsBackslash = true,
Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) },
Line = line,
Column = column,
NewLine = newLine
};
processor.Inline.Span.End = processor.Inline.Span.Start + 1;
slice.SkipChar();
return true;
newLine = NewLine.CarriageReturnLineFeed;
}
}
else
{
if (c == '\n' || c == '\r')
var inline = new LineBreakInline()
{
processor.Inline = new LineBreakInline()
{
IsHard = true,
IsBackslash = true,
Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) },
Line = line,
Column = column
};
processor.Inline.Span.End = processor.Inline.Span.Start + 1;
slice.SkipChar();
return true;
IsHard = true,
IsBackslash = true,
Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) },
Line = line,
Column = column,
};
processor.Inline = inline;
if (processor.TrackTrivia)
{
inline.NewLine = newLine;
}
inline.Span.End = inline.Span.Start + 1;
slice.SkipChar(); // Skip \n or \r alone
if (newLine == NewLine.CarriageReturnLineFeed)
{
slice.SkipChar(); // Skip \r\n
}
return true;
}
return false;

View File

@@ -68,12 +68,14 @@ namespace Markdig.Parsers.Inlines
// The LiteralInlineParser is always matching (at least an empty string)
var endPosition = slice.Start + length - 1;
if (processor.Inline is LiteralInline previousInline
&& ReferenceEquals(previousInline.Content.Text, slice.Text)
&& previousInline.Content.End + 1 == slice.Start)
if (processor.Inline is { } previousInline
&& !previousInline.IsContainer
&& processor.Inline is LiteralInline previousLiteral
&& ReferenceEquals(previousLiteral.Content.Text, slice.Text)
&& previousLiteral.Content.End + 1 == slice.Start)
{
previousInline.Content.End = endPosition;
previousInline.Span.End = processor.GetSourcePosition(endPosition);
previousLiteral.Content.End = endPosition;
previousLiteral.Span.End = processor.GetSourcePosition(endPosition);
}
else
{

View File

@@ -86,7 +86,7 @@ namespace Markdig.Parsers
// interpretations of a line, the thematic break takes precedence
BlockState result;
var thematicParser = ThematicBreakParser.Default;
if (!(processor.LastBlock is FencedCodeBlock) && thematicParser.HasOpeningCharacter(processor.CurrentChar))
if (processor.LastBlock is not FencedCodeBlock && thematicParser.HasOpeningCharacter(processor.CurrentChar))
{
result = thematicParser.TryOpen(processor);
if (result.IsBreak())

View File

@@ -3,6 +3,7 @@
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using System;
namespace Markdig.Parsers
{
@@ -56,7 +57,16 @@ namespace Markdig.Parsers
return false;
}
result.OrderedStart = state.Line.Text.Substring(startChar, endChar - startChar + 1);
if (startChar == endChar)
{
// Common case: a single digit character
result.OrderedStart = CharHelper.SmallNumberToString(state.Line.Text[startChar] - '0');
}
else
{
result.OrderedStart = state.Line.Text.Substring(startChar, endChar - startChar + 1);
}
result.OrderedDelimiter = orderedDelimiter;
result.BulletType = '1';
result.DefaultOrderedStart = "1";

View File

@@ -0,0 +1,16 @@
// 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.
#if NET452 || NETSTANDARD2_0
namespace System.Collections.Concurrent
{
internal static class ConcurrentQueueExtensions
{
public static void Clear<T>(this ConcurrentQueue<T> queue)
{
while (queue.TryDequeue(out _)) { }
}
}
}
#endif

View File

@@ -22,11 +22,11 @@ namespace Markdig.Renderers.Html.Inlines
}
if (renderer.EnableHtmlEscape)
{
renderer.WriteEscape(obj.Content);
renderer.WriteEscape(obj.ContentSpan);
}
else
{
renderer.Write(obj.Content);
renderer.Write(obj.ContentSpan);
}
if (renderer.EnableHtmlForInline)
{

View File

@@ -15,15 +15,16 @@ namespace Markdig.Renderers.Normalize.Inlines
protected override void Write(NormalizeRenderer renderer, CodeInline obj)
{
var delimiterCount = 0;
for (var i = 0; i < obj.Content!.Length; i++)
string content = obj.Content;
for (var i = 0; i < content.Length; i++)
{
var index = obj.Content.IndexOf(obj.Delimiter, i);
var index = content.IndexOf(obj.Delimiter, i);
if (index == -1) break;
var count = 1;
for (i = index + 1; i < obj.Content.Length; i++)
for (i = index + 1; i < content.Length; i++)
{
if (obj.Content[i] == obj.Delimiter) count++;
if (content[i] == obj.Delimiter) count++;
else break;
}
@@ -32,14 +33,14 @@ namespace Markdig.Renderers.Normalize.Inlines
}
var delimiterRun = new string(obj.Delimiter, delimiterCount + 1);
renderer.Write(delimiterRun);
if (obj.Content.Length != 0)
if (content.Length != 0)
{
if (obj.Content[0] == obj.Delimiter)
if (content[0] == obj.Delimiter)
{
renderer.Write(' ');
}
renderer.Write(obj.Content);
if (obj.Content[obj.Content.Length - 1] == obj.Delimiter)
renderer.Write(content);
if (content[content.Length - 1] == obj.Delimiter)
{
renderer.Write(' ');
}

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Markdig.Helpers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
@@ -16,7 +17,7 @@ namespace Markdig.Renderers
/// <seealso cref="IMarkdownRenderer" />
public abstract class RendererBase : IMarkdownRenderer
{
private readonly Dictionary<RuntimeTypeHandle, IMarkdownObjectRenderer?> _renderersPerType = new();
private readonly Dictionary<KeyWrapper, IMarkdownObjectRenderer?> _renderersPerType = new();
internal int _childrenDepth = 0;
/// <summary>
@@ -26,7 +27,7 @@ namespace Markdig.Renderers
private IMarkdownObjectRenderer? GetRendererInstance(MarkdownObject obj)
{
RuntimeTypeHandle typeHandle = Type.GetTypeHandle(obj);
KeyWrapper key = GetKeyForType(obj);
Type objectType = obj.GetType();
for (int i = 0; i < ObjectRenderers.Count; i++)
@@ -34,12 +35,12 @@ namespace Markdig.Renderers
var renderer = ObjectRenderers[i];
if (renderer.Accept(this, objectType))
{
_renderersPerType[typeHandle] = renderer;
_renderersPerType[key] = renderer;
return renderer;
}
}
_renderersPerType[typeHandle] = null;
_renderersPerType[key] = null;
return null;
}
@@ -140,7 +141,7 @@ namespace Markdig.Renderers
// Calls before writing an object
ObjectWriteBefore?.Invoke(this, obj);
if (!_renderersPerType.TryGetValue(Type.GetTypeHandle(obj), out IMarkdownObjectRenderer? renderer))
if (!_renderersPerType.TryGetValue(GetKeyForType(obj), out IMarkdownObjectRenderer? renderer))
{
renderer = GetRendererInstance(obj);
}
@@ -149,17 +150,37 @@ namespace Markdig.Renderers
{
renderer.Write(this, obj);
}
else if (obj is ContainerInline containerInline)
else if (obj.IsContainerInline)
{
WriteChildren(containerInline);
WriteChildren(Unsafe.As<ContainerInline>(obj));
}
else if (obj is ContainerBlock containerBlock)
else if (obj.IsContainerBlock)
{
WriteChildren(containerBlock);
WriteChildren(Unsafe.As<ContainerBlock>(obj));
}
// Calls after writing an object
ObjectWriteAfter?.Invoke(this, obj);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static KeyWrapper GetKeyForType(MarkdownObject obj)
{
IntPtr typeHandle = Type.GetTypeHandle(obj).Value;
return new KeyWrapper(typeHandle);
}
private readonly struct KeyWrapper : IEquatable<KeyWrapper>
{
public readonly IntPtr Key;
public KeyWrapper(IntPtr key) => Key = key;
public bool Equals(KeyWrapper other) => Key == other.Key;
public override int GetHashCode() => Key.GetHashCode();
public override bool Equals(object? obj) => throw new NotImplementedException();
}
}
}

View File

@@ -16,7 +16,7 @@ namespace Markdig.Renderers.Roundtrip.Inlines
{
var delimiterRun = new string(obj.Delimiter, obj.DelimiterCount);
renderer.Write(delimiterRun);
if (obj.Content is { Length: > 0 })
if (!obj.ContentSpan.IsEmpty)
{
renderer.Write(obj.ContentWithTrivia);
}

View File

@@ -14,8 +14,8 @@ namespace Markdig.Syntax
/// <seealso cref="MarkdownObject" />
public abstract class Block : MarkdownObject, IBlock
{
private BlockTriviaProperties? _trivia;
private BlockTriviaProperties Trivia => _trivia ??= new();
private BlockTriviaProperties? _trivia => GetTrivia<BlockTriviaProperties>();
private BlockTriviaProperties Trivia => GetOrSetTrivia<BlockTriviaProperties>();
/// <summary>
/// Initializes a new instance of the <see cref="Block"/> class.
@@ -26,6 +26,7 @@ namespace Markdig.Syntax
Parser = parser;
IsOpen = true;
IsBreakable = true;
SetTypeKind(isInline: false, isContainer: false);
}
/// <summary>
@@ -38,8 +39,8 @@ namespace Markdig.Syntax
/// </summary>
public BlockParser? Parser { get; }
internal bool IsContainerBlock { get; private protected set; }
internal bool IsLeafBlock { get; private protected set; }
internal bool IsParagraphBlock { get; private protected set; }
/// <summary>

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Markdig.Helpers;
using Markdig.Parsers;
@@ -19,7 +20,7 @@ namespace Markdig.Syntax
[DebuggerDisplay("{GetType().Name} Count = {Count}")]
public abstract class ContainerBlock : Block, IList<Block>, IReadOnlyList<Block>
{
private Block[] children;
private BlockWrapper[] _children;
/// <summary>
/// Initializes a new instance of the <see cref="ContainerBlock"/> class.
@@ -27,14 +28,30 @@ namespace Markdig.Syntax
/// <param name="parser">The parser used to create this block.</param>
protected ContainerBlock(BlockParser? parser) : base(parser)
{
children = ArrayHelper.Empty<Block>();
IsContainerBlock = true;
_children = ArrayHelper.Empty<BlockWrapper>();
SetTypeKind(isInline: false, isContainer: true);
}
/// <summary>
/// Gets the last child.
/// </summary>
public Block? LastChild => Count > 0 ? children[Count - 1] : null;
public Block? LastChild
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
BlockWrapper[] children = _children;
int index = Count - 1;
if ((uint)index < (uint)children.Length)
{
return children[index].Block;
}
else
{
return null;
}
}
}
/// <summary>
/// Specialize enumerator.
@@ -65,78 +82,65 @@ namespace Markdig.Syntax
ThrowHelper.ArgumentException("Cannot add this block as it as already attached to another container (block.Parent != null)");
}
if (Count == children.Length)
if (Count == _children.Length)
{
EnsureCapacity(Count + 1);
Grow();
}
children[Count++] = item;
_children[Count] = new BlockWrapper(item);
Count++;
item.Parent = this;
UpdateSpanEnd(item.Span.End);
}
private void EnsureCapacity(int min)
private void Grow()
{
if (children.Length < min)
if (_children.Length == 0)
{
int num = (children.Length == 0) ? 4 : (children.Length * 2);
if (num < min)
{
num = min;
}
_children = new BlockWrapper[4];
}
else
{
Debug.Assert(_children[_children.Length - 1].Block is not null);
var destinationArray = new Block[num];
if (Count > 0)
{
Array.Copy(children, 0, destinationArray, 0, Count);
}
children = destinationArray;
var newArray = new BlockWrapper[_children.Length * 2];
Array.Copy(_children, 0, newArray, 0, Count);
_children = newArray;
}
}
public void Clear()
{
for (int i = 0; i < Count; i++)
BlockWrapper[] children = _children;
for (int i = 0; i < Count && i < children.Length; i++)
{
children[i].Parent = null;
children[i] = null!;
children[i].Block.Parent = null;
children[i] = default;
}
Count = 0;
}
public bool Contains(Block item)
{
if (item is null)
ThrowHelper.ArgumentNullException_item();
for (int i = 0; i < Count; i++)
{
if (children[i] == item)
{
return true;
}
}
return false;
return IndexOf(item) >= 0;
}
public void CopyTo(Block[] array, int arrayIndex)
{
Array.Copy(children, 0, array, arrayIndex, Count);
BlockWrapper[] children = _children;
for (int i = 0; i < Count && i < children.Length; i++)
{
array[arrayIndex + i] = children[i].Block;
}
}
public bool Remove(Block item)
{
if (item is null)
ThrowHelper.ArgumentNullException_item();
for (int i = Count - 1; i >= 0; i--)
int index = IndexOf(item);
if (index >= 0)
{
if (children[i] == item)
{
RemoveAt(i);
return true;
}
RemoveAt(index);
return true;
}
return false;
}
@@ -150,9 +154,10 @@ namespace Markdig.Syntax
if (item is null)
ThrowHelper.ArgumentNullException_item();
for (int i = 0; i < Count; i++)
BlockWrapper[] children = _children;
for (int i = 0; i < Count && i < children.Length; i++)
{
if (children[i] == item)
if (ReferenceEquals(children[i].Block, item))
{
return i;
}
@@ -173,46 +178,47 @@ namespace Markdig.Syntax
{
ThrowHelper.ArgumentOutOfRangeException_index();
}
if (Count == children.Length)
if (Count == _children.Length)
{
EnsureCapacity(Count + 1);
Grow();
}
if (index < Count)
{
Array.Copy(children, index, children, index + 1, Count - index);
Array.Copy(_children, index, _children, index + 1, Count - index);
}
children[index] = item;
_children[index] = new BlockWrapper(item);
Count++;
item.Parent = this;
}
public void RemoveAt(int index)
{
if ((uint)index > (uint)Count)
if ((uint)index >= (uint)Count)
ThrowHelper.ArgumentOutOfRangeException_index();
Count--;
// previous children
var item = children[index];
var item = _children[index].Block;
item.Parent = null;
if (index < Count)
{
Array.Copy(children, index + 1, children, index, Count - index);
Array.Copy(_children, index + 1, _children, index, Count - index);
}
children[Count] = null!;
_children[Count] = default;
}
public Block this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
var array = children;
var array = _children;
if ((uint)index >= (uint)array.Length || index >= Count)
{
ThrowHelper.ThrowIndexOutOfRangeException();
return null;
}
return array[index];
return array[index].Block;
}
set
{
@@ -224,25 +230,25 @@ namespace Markdig.Syntax
if (value.Parent != null)
ThrowHelper.ArgumentException("Cannot add this block as it as already attached to another container (block.Parent != null)");
var existingChild = children[index];
var existingChild = _children[index].Block;
if (existingChild != null)
existingChild.Parent = null;
value.Parent = this;
children[index] = value;
_children[index] = new BlockWrapper(value);
}
}
public void Sort(IComparer<Block> comparer)
{
if (comparer is null) ThrowHelper.ArgumentNullException(nameof(comparer));
Array.Sort(children, 0, Count, comparer);
Array.Sort(_children, 0, Count, new BlockComparerWrapper(comparer));
}
public void Sort(Comparison<Block> comparison)
{
if (comparison is null) ThrowHelper.ArgumentNullException(nameof(comparison));
Array.Sort(children, 0, Count, new BlockComparer(comparison));
Array.Sort(_children, 0, Count, new BlockComparisonWrapper(comparison));
}
#region Nested type: Enumerator
@@ -296,18 +302,33 @@ namespace Markdig.Syntax
#endregion
private sealed class BlockComparer : IComparer<Block>
private sealed class BlockComparisonWrapper : IComparer<BlockWrapper>
{
private readonly Comparison<Block> comparison;
private readonly Comparison<Block> _comparison;
public BlockComparer(Comparison<Block> comparison)
public BlockComparisonWrapper(Comparison<Block> comparison)
{
this.comparison = comparison;
_comparison = comparison;
}
public int Compare(Block? x, Block? y)
public int Compare(BlockWrapper x, BlockWrapper y)
{
return comparison(x!, y!);
return _comparison(x.Block, y.Block);
}
}
private sealed class BlockComparerWrapper : IComparer<BlockWrapper>
{
private readonly IComparer<Block> _comparer;
public BlockComparerWrapper(IComparer<Block> comparer)
{
_comparer = comparer;
}
public int Compare(BlockWrapper x, BlockWrapper y)
{
return _comparer.Compare(x.Block, y.Block);
}
}
}

View File

@@ -3,6 +3,7 @@
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using System;
using System.Diagnostics;
namespace Markdig.Syntax.Inlines
@@ -14,12 +15,16 @@ namespace Markdig.Syntax.Inlines
[DebuggerDisplay("`{Content}`")]
public class CodeInline : LeafInline
{
private TriviaProperties? _trivia;
private TriviaProperties Trivia => _trivia ??= new();
private TriviaProperties? _trivia => GetTrivia<TriviaProperties>();
private TriviaProperties Trivia => GetOrSetTrivia<TriviaProperties>();
public CodeInline(string content)
private LazySubstring _content;
public CodeInline(string content) : this(new LazySubstring(content)) { }
internal CodeInline(LazySubstring content)
{
Content = content;
_content = content;
}
/// <summary>
@@ -35,7 +40,13 @@ namespace Markdig.Syntax.Inlines
/// <summary>
/// Gets or sets the content of the span.
/// </summary>
public string Content { get; set; }
public string Content
{
get => _content.ToString();
set => _content = new LazySubstring(value ?? string.Empty);
}
public ReadOnlySpan<char> ContentSpan => _content.AsSpan();
/// <summary>
/// Gets or sets the content with trivia and whitespace.

View File

@@ -19,7 +19,7 @@ namespace Markdig.Syntax.Inlines
{
public ContainerInline()
{
IsContainerInline = true;
SetTypeKind(isInline: true, isContainer: true);
}
/// <summary>

View File

@@ -10,7 +10,7 @@ namespace Markdig.Syntax.Inlines
/// Gets the type of a <see cref="DelimiterInline"/>.
/// </summary>
[Flags]
public enum DelimiterType
public enum DelimiterType : byte
{
/// <summary>
/// An undefined open or close delimiter.

View File

@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using Markdig.Helpers;
namespace Markdig.Syntax.Inlines
@@ -15,6 +16,11 @@ namespace Markdig.Syntax.Inlines
/// <seealso cref="MarkdownObject" />
public abstract class Inline : MarkdownObject, IInline
{
protected Inline()
{
SetTypeKind(isInline: true, isContainer: false);
}
/// <summary>
/// Gets the parent container of this inline.
/// </summary>
@@ -30,12 +36,14 @@ namespace Markdig.Syntax.Inlines
/// </summary>
public Inline? NextSibling { get; internal set; }
internal bool IsContainerInline { get; private protected set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is closed.
/// </summary>
public bool IsClosed { get; set; }
public bool IsClosed
{
get => IsClosedInternal;
set => IsClosedInternal = value;
}
/// <summary>
/// Inserts the specified inline after this instance.
@@ -153,15 +161,14 @@ namespace Markdig.Syntax.Inlines
parent.AppendChild(inline);
}
var container = this as ContainerInline;
if (copyChildren && container != null)
if (copyChildren && IsContainerInline)
{
var newContainer = inline as ContainerInline;
// Don't append to a closed container
if (newContainer != null && newContainer.IsClosed)
{
newContainer = null;
}
var container = Unsafe.As<ContainerInline>(this);
ContainerInline? newContainer = inline.IsContainerInline && !inline.IsClosed
? Unsafe.As<ContainerInline>(inline)
: null;
// TODO: This part is not efficient as it is using child.Remove()
// We need a method to quickly move all children without having to mess Next/Prev sibling
var child = container.FirstChild;

View File

@@ -13,8 +13,8 @@ namespace Markdig.Syntax.Inlines
/// <seealso cref="DelimiterInline" />
public class LinkDelimiterInline : DelimiterInline
{
private TriviaProperties? _trivia;
private TriviaProperties Trivia => _trivia ??= new();
private TriviaProperties? _trivia => GetTrivia<TriviaProperties>();
private TriviaProperties Trivia => GetOrSetTrivia<TriviaProperties>();
public LinkDelimiterInline(InlineParser parser) : base(parser)
{

View File

@@ -21,8 +21,8 @@ namespace Markdig.Syntax.Inlines
[DebuggerDisplay("Url: {Url} Title: {Title} Image: {IsImage}")]
public class LinkInline : ContainerInline
{
private TriviaProperties? _trivia;
private TriviaProperties Trivia => _trivia ??= new();
private TriviaProperties? _trivia => GetTrivia<TriviaProperties>();
private TriviaProperties Trivia => GetOrSetTrivia<TriviaProperties>();
/// <summary>
/// A delegate to use if it is setup on this instance to allow late binding
@@ -62,7 +62,7 @@ namespace Markdig.Syntax.Inlines
/// <summary>
/// The label span
/// </summary>
public SourceSpan? LabelSpan;
public SourceSpan LabelSpan;
/// <summary>
/// Gets or sets the <see cref="Label"/> with trivia.
@@ -118,7 +118,7 @@ namespace Markdig.Syntax.Inlines
/// <summary>
/// The URL source span.
/// </summary>
public SourceSpan? UrlSpan;
public SourceSpan UrlSpan;
/// <summary>
/// The <see cref="Url"/> but with trivia and unescaped characters
@@ -154,7 +154,7 @@ namespace Markdig.Syntax.Inlines
/// <summary>
/// The title source span.
/// </summary>
public SourceSpan? TitleSpan;
public SourceSpan TitleSpan;
/// <summary>
/// Gets or sets the <see cref="Title"/> exactly as parsed from the

View File

@@ -51,7 +51,11 @@ namespace Markdig.Syntax.Inlines
/// <summary>
/// A boolean indicating whether the first character of this literal is escaped by `\`.
/// </summary>
public bool IsFirstCharacterEscaped { get; set; }
public bool IsFirstCharacterEscaped
{
get => InternalSpareBit;
set => InternalSpareBit = value;
}
public override string ToString()
{

View File

@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Markdig.Helpers;
namespace Markdig.Syntax
@@ -15,12 +16,18 @@ namespace Markdig.Syntax
/// <seealso cref="ContainerBlock" />
public class LinkReferenceDefinitionGroup : ContainerBlock
{
#if NET452
private static readonly StringComparer _unicodeIgnoreCaseComparer = StringComparer.InvariantCultureIgnoreCase;
#else
private static readonly StringComparer _unicodeIgnoreCaseComparer = CultureInfo.InvariantCulture.CompareInfo.GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace);
#endif
/// <summary>
/// Initializes a new instance of the <see cref="LinkReferenceDefinitionGroup"/> class.
/// </summary>
public LinkReferenceDefinitionGroup() : base(null)
{
Links = new Dictionary<string, LinkReferenceDefinition>(StringComparer.OrdinalIgnoreCase);
Links = new Dictionary<string, LinkReferenceDefinition>(_unicodeIgnoreCaseComparer);
}
/// <summary>

View File

@@ -3,6 +3,7 @@
// See the license.txt file in the project root for more information.
using System;
using System.Runtime.CompilerServices;
using Markdig.Helpers;
namespace Markdig.Syntax
@@ -12,6 +13,54 @@ namespace Markdig.Syntax
/// </summary>
public abstract class MarkdownObject : IMarkdownObject
{
private const uint ValueBitMask = (1u << 30) - 1;
private const uint FirstBitMask = 1u << 31;
private const uint SecondBitMask = 1u << 30;
private const uint IsInlineMask = FirstBitMask;
private const uint IsContainerMask = SecondBitMask;
private const uint TypeKindMask = IsInlineMask | IsContainerMask;
// Limit the value to 30 bits and repurpose the last two bits for commonly used flags
private uint _lineBits; // Also stores TypeKindMask (IsInline and IsContainer)
private uint _columnBits; // Also stores IsClosedInternal and InternalSpareBit
internal bool IsContainerInline => (_lineBits & TypeKindMask) == (IsContainerMask | IsInlineMask);
internal bool IsContainerBlock => (_lineBits & TypeKindMask) == IsContainerMask;
internal bool IsContainer => (_lineBits & IsContainerMask) != 0;
internal bool IsInline => (_lineBits & IsInlineMask) != 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected void SetTypeKind(bool isInline, bool isContainer)
{
_lineBits |= (isInline ? IsInlineMask : 0) | (isContainer ? IsContainerMask : 0);
}
private protected bool IsClosedInternal
{
get => (_columnBits & FirstBitMask) != 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
if (value) _columnBits |= FirstBitMask;
else _columnBits &= ~FirstBitMask;
}
}
private protected bool InternalSpareBit
{
get => (_columnBits & SecondBitMask) != 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
if (value) _columnBits |= SecondBitMask;
else _columnBits &= ~SecondBitMask;
}
}
protected MarkdownObject()
{
Span = SourceSpan.Empty;
@@ -22,17 +71,25 @@ namespace Markdig.Syntax
/// as we expect less than 5~10 entries, usually typically 1 (HtmlAttributes)
/// so it will gives faster access than a Dictionary, and lower memory occupation
/// </summary>
private DataEntries? _attachedDatas;
private DataEntriesAndTrivia? _attachedDatas;
/// <summary>
/// Gets or sets the text column this instance was declared (zero-based).
/// </summary>
public int Column { get; set; }
public int Column
{
get => (int)(_columnBits & ValueBitMask);
set => _columnBits = (_columnBits & ~ValueBitMask) | ((uint)value & ValueBitMask);
}
/// <summary>
/// Gets or sets the text line this instance was declared (zero-based).
/// </summary>
public int Line { get; set; }
public int Line
{
get => (int)(_lineBits & ValueBitMask);
set => _lineBits = (_lineBits & ~ValueBitMask) | ((uint)value & ValueBitMask);
}
/// <summary>
/// The source span
@@ -54,7 +111,7 @@ namespace Markdig.Syntax
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
/// <exception cref="ArgumentNullException">if key is null</exception>
public void SetData(object key, object value) => (_attachedDatas ??= new DataEntries()).SetData(key, value);
public void SetData(object key, object value) => (_attachedDatas ??= new DataEntriesAndTrivia()).SetData(key, value);
/// <summary>
/// Determines whether this instance contains the specified key data.
@@ -80,7 +137,21 @@ namespace Markdig.Syntax
/// <exception cref="ArgumentNullException"></exception>
public bool RemoveData(object key) => _attachedDatas?.RemoveData(key) ?? false;
private class DataEntries
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected T? GetTrivia<T>() where T : class
{
object? trivia = _attachedDatas?.Trivia;
return trivia is null ? null : Unsafe.As<T>(trivia);
}
private protected T GetOrSetTrivia<T>() where T : class, new()
{
var storage = _attachedDatas ??= new DataEntriesAndTrivia();
storage.Trivia ??= new T();
return Unsafe.As<T>(storage.Trivia);
}
private class DataEntriesAndTrivia
{
private struct DataEntry
{
@@ -94,37 +165,41 @@ namespace Markdig.Syntax
}
}
private DataEntry[] _entries;
private DataEntry[]? _entries;
private int _count;
public DataEntries()
{
_entries = new DataEntry[2];
}
public object? Trivia;
public void SetData(object key, object value)
{
if (key is null) ThrowHelper.ArgumentNullException_key();
DataEntry[] entries = _entries;
DataEntry[]? entries = _entries;
int count = _count;
for (int i = 0; i < entries.Length && i < count; i++)
if (entries is null)
{
ref DataEntry entry = ref entries[i];
if (entry.Key == key)
_entries = new DataEntry[2];
}
else
{
for (int i = 0; i < entries.Length && i < count; i++)
{
entry.Value = value;
return;
ref DataEntry entry = ref entries[i];
if (entry.Key == key)
{
entry.Value = value;
return;
}
}
if (count == entries.Length)
{
Array.Resize(ref _entries, count + 2);
}
}
if (count == entries.Length)
{
Array.Resize(ref _entries, count + 2);
}
_entries[count] = new DataEntry(key, value);
_entries![count] = new DataEntry(key, value);
_count++;
}
@@ -132,7 +207,12 @@ namespace Markdig.Syntax
{
if (key is null) ThrowHelper.ArgumentNullException_key();
DataEntry[] entries = _entries;
DataEntry[]? entries = _entries;
if (entries is null)
{
return null;
}
int count = _count;
for (int i = 0; i < entries.Length && i < count; i++)
@@ -151,7 +231,12 @@ namespace Markdig.Syntax
{
if (key is null) ThrowHelper.ArgumentNullException_key();
DataEntry[] entries = _entries;
DataEntry[]? entries = _entries;
if (entries is null)
{
return false;
}
int count = _count;
for (int i = 0; i < entries.Length && i < count; i++)
@@ -169,7 +254,12 @@ namespace Markdig.Syntax
{
if (key is null) ThrowHelper.ArgumentNullException_key();
DataEntry[] entries = _entries;
DataEntry[]? entries = _entries;
if (entries is null)
{
return false;
}
int count = _count;
for (int i = 0; i < entries.Length && i < count; i++)

View File

@@ -63,7 +63,7 @@ namespace SpecFileGen
// NOTE: Beware of Copy/Pasting spec files - some characters may change (non-breaking space into space)!
static readonly Spec[] Specs = new[]
{
new Spec("CommonMark v. 0.29", "CommonMark.md", ""),
new Spec("CommonMarkSpecs", "CommonMark.md", ""),
new Spec("Pipe Tables", "PipeTableSpecs.md", "pipetables|advanced"),
new Spec("GFM Pipe Tables", "PipeTableGfmSpecs.md", "gfm-pipetables"),
new Spec("Footnotes", "FootnotesSpecs.md", "footnotes|advanced"),