mirror of
https://github.com/xoofx/markdig.git
synced 2026-02-08 05:44:58 +00:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94e07d11ce | ||
|
|
263041e899 | ||
|
|
e8f9274b64 | ||
|
|
b32e71aaeb | ||
|
|
f991b2123b | ||
|
|
a4a1a177bc | ||
|
|
59d59694f4 | ||
|
|
caf3c722e1 | ||
|
|
47c64d8815 | ||
|
|
2502fab340 | ||
|
|
17b5500b03 | ||
|
|
b754aef6b0 | ||
|
|
04843a08d2 | ||
|
|
fcc73691b6 | ||
|
|
cb8dc99d96 | ||
|
|
6f45ac0885 | ||
|
|
891e2fca78 | ||
|
|
925d4f9227 | ||
|
|
d69b989810 | ||
|
|
da756f4efe | ||
|
|
e192831db0 | ||
|
|
be3c93a9b0 | ||
|
|
6466f01a80 | ||
|
|
cb26f30f7b | ||
|
|
147c3f059a | ||
|
|
3201699053 | ||
|
|
e86d1ffce5 | ||
|
|
48c979dc74 | ||
|
|
3ae0c8b369 | ||
|
|
ee732e5a42 | ||
|
|
76e25833ad | ||
|
|
53dff53260 | ||
|
|
b2eeaf7185 | ||
|
|
47a22bc5e8 | ||
|
|
89e4c29f9f | ||
|
|
a946c6d0b4 | ||
|
|
e2770d8c11 | ||
|
|
6eacf8a170 | ||
|
|
e11a2630b8 | ||
|
|
ccf455d316 | ||
|
|
8beb096814 | ||
|
|
6a35ec45b9 | ||
|
|
ed83943ba5 | ||
|
|
9adf60116b | ||
|
|
31904f6c53 | ||
|
|
3f3b3c46b6 | ||
|
|
f3d6c2775b | ||
|
|
bb6ace15b7 | ||
|
|
202ac1e4f9 | ||
|
|
2604239764 | ||
|
|
cc04208b95 | ||
|
|
14ab45cf8f | ||
|
|
e36d4564f1 | ||
|
|
358a5f09ef | ||
|
|
25db6cb414 | ||
|
|
315ffd42ab | ||
|
|
2675b4dd1e | ||
|
|
58d7fae12d | ||
|
|
e16ed79dcd | ||
|
|
9ef5171369 | ||
|
|
0cfe6d7da4 | ||
|
|
fe65c1b187 | ||
|
|
92385ee19a | ||
|
|
9f651feac0 | ||
|
|
d42b297128 | ||
|
|
b7d02cadbb | ||
|
|
6f75b5156c | ||
|
|
61452c91e9 | ||
|
|
b697a03c2b | ||
|
|
9f734ba3c9 | ||
|
|
88cdbf3a17 | ||
|
|
fb9561cf89 | ||
|
|
9145f47f89 | ||
|
|
1862b37bbd |
134
doc/parsing-ast.md
Normal file
134
doc/parsing-ast.md
Normal 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`.
|
||||
|
||||
158
doc/parsing-extensions.md
Normal file
158
doc/parsing-extensions.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# 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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Parsers
|
||||
|
||||
Markdig has 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.
|
||||
|
||||
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 Parser
|
||||
227
doc/parsing-overview.md
Normal file
227
doc/parsing-overview.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# 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);
|
||||
```
|
||||
|
||||
## The Parser and the Pipeline
|
||||
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
For a discussion on how to implement an extension, refer to the [Extensions/Parsers](parsing-extensions.md) document.
|
||||
|
||||
### 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.*
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
````````````````````````````````
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
189
src/Markdig.Tests/TestFastStringWriter.cs
Normal file
189
src/Markdig.Tests/TestFastStringWriter.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using Markdig.Helpers;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Markdig.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestFastStringWriter
|
||||
{
|
||||
private const string NewLineReplacement = "~~NEW_LINE~~";
|
||||
|
||||
private FastStringWriter _writer = new();
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_writer = new FastStringWriter
|
||||
{
|
||||
NewLine = NewLineReplacement
|
||||
};
|
||||
}
|
||||
|
||||
public void AssertToString(string value)
|
||||
{
|
||||
value = value.Replace("\n", NewLineReplacement);
|
||||
Assert.AreEqual(value, _writer.ToString());
|
||||
Assert.AreEqual(value, _writer.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NewLine()
|
||||
{
|
||||
Assert.AreEqual("\n", new FastStringWriter().NewLine);
|
||||
|
||||
_writer.NewLine = "\r";
|
||||
Assert.AreEqual("\r", _writer.NewLine);
|
||||
|
||||
_writer.NewLine = "foo";
|
||||
Assert.AreEqual("foo", _writer.NewLine);
|
||||
|
||||
_writer.WriteLine();
|
||||
await _writer.WriteLineAsync();
|
||||
_writer.WriteLine("bar");
|
||||
Assert.AreEqual("foofoobarfoo", _writer.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task FlushCloseDispose()
|
||||
{
|
||||
_writer.Write('a');
|
||||
|
||||
// Nops
|
||||
_writer.Close();
|
||||
_writer.Dispose();
|
||||
await _writer.DisposeAsync();
|
||||
_writer.Flush();
|
||||
await _writer.FlushAsync();
|
||||
|
||||
_writer.Write('b');
|
||||
AssertToString("ab");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Write_Char()
|
||||
{
|
||||
_writer.Write('a');
|
||||
AssertToString("a");
|
||||
|
||||
_writer.Write('b');
|
||||
AssertToString("ab");
|
||||
|
||||
_writer.Write('\0');
|
||||
_writer.Write('\r');
|
||||
_writer.Write('\u1234');
|
||||
AssertToString("ab\0\r\u1234");
|
||||
|
||||
_writer.Reset();
|
||||
AssertToString("");
|
||||
|
||||
_writer.Write('a');
|
||||
_writer.WriteLine('b');
|
||||
_writer.Write('c');
|
||||
_writer.Write('d');
|
||||
_writer.WriteLine('e');
|
||||
AssertToString("ab\ncde\n");
|
||||
|
||||
await _writer.WriteAsync('f');
|
||||
await _writer.WriteLineAsync('g');
|
||||
AssertToString("ab\ncde\nfg\n");
|
||||
|
||||
_writer.Reset();
|
||||
|
||||
for (int i = 0; i < 2050; i++)
|
||||
{
|
||||
_writer.Write('a');
|
||||
AssertToString(new string('a', i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Write_String()
|
||||
{
|
||||
_writer.Write("foo");
|
||||
AssertToString("foo");
|
||||
|
||||
_writer.WriteLine("bar");
|
||||
AssertToString("foobar\n");
|
||||
|
||||
await _writer.WriteAsync("baz");
|
||||
await _writer.WriteLineAsync("foo");
|
||||
AssertToString("foobar\nbazfoo\n");
|
||||
|
||||
_writer.Write(new string('a', 1050));
|
||||
AssertToString("foobar\nbazfoo\n" + new string('a', 1050));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Write_Span()
|
||||
{
|
||||
_writer.Write("foo".AsSpan());
|
||||
AssertToString("foo");
|
||||
|
||||
_writer.WriteLine("bar".AsSpan());
|
||||
AssertToString("foobar\n");
|
||||
|
||||
await _writer.WriteAsync("baz".AsMemory());
|
||||
await _writer.WriteLineAsync("foo".AsMemory());
|
||||
AssertToString("foobar\nbazfoo\n");
|
||||
|
||||
_writer.Write(new string('a', 1050).AsSpan());
|
||||
AssertToString("foobar\nbazfoo\n" + new string('a', 1050));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Write_CharArray()
|
||||
{
|
||||
_writer.Write("foo".ToCharArray());
|
||||
AssertToString("foo");
|
||||
|
||||
_writer.WriteLine("bar".ToCharArray());
|
||||
AssertToString("foobar\n");
|
||||
|
||||
await _writer.WriteAsync("baz".ToCharArray());
|
||||
await _writer.WriteLineAsync("foo".ToCharArray());
|
||||
AssertToString("foobar\nbazfoo\n");
|
||||
|
||||
_writer.Write(new string('a', 1050).ToCharArray());
|
||||
AssertToString("foobar\nbazfoo\n" + new string('a', 1050));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Write_CharArrayWithIndexes()
|
||||
{
|
||||
_writer.Write("foo".ToCharArray(), 1, 1);
|
||||
AssertToString("o");
|
||||
|
||||
_writer.WriteLine("bar".ToCharArray(), 0, 2);
|
||||
AssertToString("oba\n");
|
||||
|
||||
await _writer.WriteAsync("baz".ToCharArray(), 0, 1);
|
||||
await _writer.WriteLineAsync("foo".ToCharArray(), 0, 3);
|
||||
AssertToString("oba\nbfoo\n");
|
||||
|
||||
_writer.Write(new string('a', 1050).ToCharArray(), 10, 1035);
|
||||
AssertToString("oba\nbfoo\n" + new string('a', 1035));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Write_StringBuilder()
|
||||
{
|
||||
_writer.Write(new StringBuilder("foo"));
|
||||
AssertToString("foo");
|
||||
|
||||
_writer.WriteLine(new StringBuilder("bar"));
|
||||
AssertToString("foobar\n");
|
||||
|
||||
await _writer.WriteAsync(new StringBuilder("baz"));
|
||||
await _writer.WriteLineAsync(new StringBuilder("foo"));
|
||||
AssertToString("foobar\nbazfoo\n");
|
||||
|
||||
var sb = new StringBuilder("foo");
|
||||
sb.Append('a', 1050);
|
||||
_writer.Write(sb);
|
||||
AssertToString("foobar\nbazfoo\nfoo" + new string('a', 1050));
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/Markdig.Tests/TestFencedCodeBlocks.cs
Normal file
46
src/Markdig.Tests/TestFencedCodeBlocks.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Linq;
|
||||
using Markdig.Syntax;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Markdig.Tests
|
||||
{
|
||||
public class TestFencedCodeBlocks
|
||||
{
|
||||
[Test]
|
||||
[TestCase("c#", "c#", "")]
|
||||
[TestCase("C#", "C#", "")]
|
||||
[TestCase(" c#", "c#", "")]
|
||||
[TestCase(" c# ", "c#", "")]
|
||||
[TestCase(" \tc# ", "c#", "")]
|
||||
[TestCase("\t c# \t", "c#", "")]
|
||||
[TestCase(" c# ", "c#", "")]
|
||||
[TestCase(" c# foo", "c#", "foo")]
|
||||
[TestCase(" c# \t fOo \t", "c#", "fOo")]
|
||||
[TestCase("in\\%fo arg\\%ument", "in%fo", "arg%ument")]
|
||||
[TestCase("info	 arg´ument", "info\t", "arg\u00B4ument")]
|
||||
public void TestInfoAndArguments(string infoString, string expectedInfo, string expectedArguments)
|
||||
{
|
||||
Test('`');
|
||||
Test('~');
|
||||
|
||||
void Test(char fencedChar)
|
||||
{
|
||||
const string Contents = "Foo\nBar\n";
|
||||
|
||||
string fence = new string(fencedChar, 3);
|
||||
string markdownText = $"{fence}{infoString}\n{Contents}\n{fence}\n";
|
||||
|
||||
MarkdownDocument document = Markdown.Parse(markdownText);
|
||||
|
||||
FencedCodeBlock codeBlock = document.Descendants<FencedCodeBlock>().Single();
|
||||
|
||||
Assert.AreEqual(fencedChar, codeBlock.FencedChar);
|
||||
Assert.AreEqual(3, codeBlock.OpeningFencedCharCount);
|
||||
Assert.AreEqual(3, codeBlock.ClosingFencedCharCount);
|
||||
Assert.AreEqual(expectedInfo, codeBlock.Info);
|
||||
Assert.AreEqual(expectedArguments, codeBlock.Arguments);
|
||||
Assert.AreEqual(Contents, codeBlock.Lines.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/Markdig.Tests/TestLazySubstring.cs
Normal file
67
src/Markdig.Tests/TestLazySubstring.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ namespace Markdig.Tests
|
||||
[TestFixture]
|
||||
public class TestPlayParser
|
||||
{
|
||||
[Test]
|
||||
public void TestBugWithEmphasisAndTable()
|
||||
{
|
||||
TestParser.TestSpec("**basics | 8:00**", "<p><strong>basics | 8:00</strong></p>", "advanced");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLinksWithCarriageReturn()
|
||||
{
|
||||
|
||||
102
src/Markdig.Tests/TestTransformedStringCache.cs
Normal file
102
src/Markdig.Tests/TestTransformedStringCache.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Markdig.Helpers;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Markdig.Tests
|
||||
{
|
||||
public class TestTransformedStringCache
|
||||
{
|
||||
[Test]
|
||||
public void GetRunsTransformationCallback()
|
||||
{
|
||||
var cache = new TransformedStringCache(static s => "callback-" + s);
|
||||
|
||||
Assert.AreEqual("callback-foo", cache.Get("foo"));
|
||||
Assert.AreEqual("callback-bar", cache.Get("bar"));
|
||||
Assert.AreEqual("callback-baz", cache.Get("baz"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CachesTransformedInstance()
|
||||
{
|
||||
var cache = new TransformedStringCache(static s => "callback-" + s);
|
||||
|
||||
string transformedBar = cache.Get("bar");
|
||||
Assert.AreSame(transformedBar, cache.Get("bar"));
|
||||
|
||||
string transformedFoo = cache.Get("foo".AsSpan());
|
||||
Assert.AreSame(transformedFoo, cache.Get("foo"));
|
||||
|
||||
Assert.AreSame(cache.Get("baz"), cache.Get("baz".AsSpan()));
|
||||
|
||||
Assert.AreSame(transformedBar, cache.Get("bar"));
|
||||
Assert.AreSame(transformedFoo, cache.Get("foo"));
|
||||
Assert.AreSame(transformedBar, cache.Get("bar".AsSpan()));
|
||||
Assert.AreSame(transformedFoo, cache.Get("foo".AsSpan()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DoesNotCacheEmptyInputs()
|
||||
{
|
||||
var cache = new TransformedStringCache(static s => new string('a', 4));
|
||||
|
||||
string cached = cache.Get("");
|
||||
string cached2 = cache.Get("");
|
||||
string cached3 = cache.Get(ReadOnlySpan<char>.Empty);
|
||||
|
||||
Assert.AreEqual("aaaa", cached);
|
||||
Assert.AreEqual(cached, cached2);
|
||||
Assert.AreEqual(cached, cached3);
|
||||
|
||||
Assert.AreNotSame(cached, cached2);
|
||||
Assert.AreNotSame(cached, cached3);
|
||||
Assert.AreNotSame(cached2, cached3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(TransformedStringCache.InputLengthLimit, true)]
|
||||
[TestCase(TransformedStringCache.InputLengthLimit + 1, false)]
|
||||
public void DoesNotCacheLongInputs(int length, bool shouldBeCached)
|
||||
{
|
||||
var cache = new TransformedStringCache(static s => "callback-" + s);
|
||||
|
||||
string input = new string('a', length);
|
||||
|
||||
string cached = cache.Get(input);
|
||||
string cached2 = cache.Get(input);
|
||||
|
||||
Assert.AreEqual("callback-" + input, cached);
|
||||
Assert.AreEqual(cached, cached2);
|
||||
|
||||
if (shouldBeCached)
|
||||
{
|
||||
Assert.AreSame(cached, cached2);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.AreNotSame(cached, cached2);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CachesAtMostNEntriesPerCharacter()
|
||||
{
|
||||
var cache = new TransformedStringCache(static s => "callback-" + s);
|
||||
|
||||
int limit = TransformedStringCache.MaxEntriesPerCharacter;
|
||||
|
||||
string[] a = Enumerable.Range(1, limit + 1).Select(i => $"a{i}").ToArray();
|
||||
string[] cachedAs = a.Select(a => cache.Get(a)).ToArray();
|
||||
|
||||
for (int i = 0; i < limit; i++)
|
||||
{
|
||||
Assert.AreSame(cachedAs[i], cache.Get(a[i]));
|
||||
}
|
||||
|
||||
Assert.AreNotSame(cachedAs[limit], cache.Get(a[limit]));
|
||||
|
||||
Assert.AreSame(cache.Get("b1"), cache.Get("b1"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,17 +172,22 @@ namespace Markdig.Extensions.AutoIdentifiers
|
||||
var baseHeadingId = string.IsNullOrEmpty(headingText) ? "section" : headingText;
|
||||
|
||||
// Add a trailing -1, -2, -3...etc. in case of collision
|
||||
int index = 0;
|
||||
var headingId = baseHeadingId;
|
||||
var headingBuffer = StringBuilderCache.Local();
|
||||
while (!identifiers.Add(headingId))
|
||||
if (!identifiers.Add(headingId))
|
||||
{
|
||||
index++;
|
||||
var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
headingBuffer.Append(baseHeadingId);
|
||||
headingBuffer.Append('-');
|
||||
headingBuffer.Append(index);
|
||||
headingId = headingBuffer.ToString();
|
||||
headingBuffer.Length = 0;
|
||||
uint index = 0;
|
||||
do
|
||||
{
|
||||
index++;
|
||||
headingBuffer.Append(index);
|
||||
headingId = headingBuffer.AsSpan().ToString();
|
||||
headingBuffer.Length = baseHeadingId.Length + 1;
|
||||
}
|
||||
while (!identifiers.Add(headingId));
|
||||
headingBuffer.Dispose();
|
||||
}
|
||||
|
||||
attributes.Id = headingId;
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Normalize;
|
||||
using Markdig.Renderers.Normalize.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.AutoLinks
|
||||
@@ -33,10 +31,6 @@ namespace Markdig.Extensions.AutoLinks
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
if (renderer is NormalizeRenderer normalizeRenderer && !normalizeRenderer.ObjectRenderers.Contains<NormalizeAutoLinkRenderer>())
|
||||
{
|
||||
normalizeRenderer.ObjectRenderers.InsertBefore<LinkInlineRenderer>(new NormalizeAutoLinkRenderer());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// 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.Renderers;
|
||||
using Markdig.Renderers.Normalize;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.AutoLinks
|
||||
{
|
||||
public class NormalizeAutoLinkRenderer : NormalizeObjectRenderer<LinkInline>
|
||||
{
|
||||
public override bool Accept(RendererBase renderer, MarkdownObject obj)
|
||||
{
|
||||
if (base.Accept(renderer, obj))
|
||||
{
|
||||
return renderer is NormalizeRenderer normalizeRenderer
|
||||
&& obj is LinkInline link
|
||||
&& !normalizeRenderer.Options.ExpandAutoLinks
|
||||
&& link.IsAutoLink;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
protected override void Write(NormalizeRenderer renderer, LinkInline obj)
|
||||
{
|
||||
renderer.Write(obj.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -95,13 +95,19 @@ namespace Markdig.Extensions.JiraLinks
|
||||
jiraLink.Span.End = jiraLink.Span.Start + (endIssue - startKey);
|
||||
|
||||
// Builds the Url
|
||||
var builder = StringBuilderCache.Local();
|
||||
builder.Append(_baseUrl).Append('/').Append(jiraLink.ProjectKey).Append('-').Append(jiraLink.Issue);
|
||||
jiraLink.Url = builder.ToString();
|
||||
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
builder.Append(_baseUrl);
|
||||
builder.Append('/');
|
||||
builder.Append(jiraLink.ProjectKey.AsSpan());
|
||||
builder.Append('-');
|
||||
builder.Append(jiraLink.Issue.AsSpan());
|
||||
jiraLink.Url = builder.AsSpan().ToString();
|
||||
|
||||
// Builds the Label
|
||||
builder.Length = 0;
|
||||
builder.Append(jiraLink.ProjectKey).Append('-').Append(jiraLink.Issue);
|
||||
builder.Append(jiraLink.ProjectKey.AsSpan());
|
||||
builder.Append('-');
|
||||
builder.Append(jiraLink.Issue.AsSpan());
|
||||
jiraLink.AppendChild(new LiteralInline(builder.ToString()));
|
||||
|
||||
if (_options.OpenInNewWindow)
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System.Text;
|
||||
using Markdig.Helpers;
|
||||
using System;
|
||||
|
||||
namespace Markdig.Extensions.JiraLinks
|
||||
{
|
||||
@@ -38,20 +39,10 @@ namespace Markdig.Extensions.JiraLinks
|
||||
/// </summary>
|
||||
public virtual string GetUrl()
|
||||
{
|
||||
var url = new StringBuilder();
|
||||
var baseUrl = BaseUrl;
|
||||
if (baseUrl != null)
|
||||
{
|
||||
url.Append(baseUrl.TrimEnd('/'));
|
||||
}
|
||||
|
||||
url.Append("/");
|
||||
|
||||
if (BasePath != null)
|
||||
{
|
||||
url.Append(BasePath.Trim('/'));
|
||||
}
|
||||
|
||||
var url = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
url.Append(BaseUrl.AsSpan().TrimEnd('/'));
|
||||
url.Append('/');
|
||||
url.Append(BasePath.AsSpan().Trim('/'));
|
||||
return url.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Markdig.Extensions.Tables
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
// Only working on Paragraph block
|
||||
if (!(processor.Block is ParagraphBlock))
|
||||
if (!processor.Block!.IsParagraphBlock)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
30
src/Markdig/Helpers/BlockWrapper.cs
Normal file
30
src/Markdig/Helpers/BlockWrapper.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -744,7 +744,7 @@ namespace Markdig.Helpers
|
||||
return cache[number];
|
||||
}
|
||||
|
||||
return number.ToString();
|
||||
return number.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Markdig.Helpers
|
||||
{
|
||||
@@ -81,7 +80,7 @@ namespace Markdig.Helpers
|
||||
});
|
||||
}
|
||||
|
||||
public static void DecodeEntity(int utf32, StringBuilder sb)
|
||||
internal static void DecodeEntity(int utf32, ref ValueStringBuilder sb)
|
||||
{
|
||||
if (!CharHelper.IsInInclusiveRange(utf32, 1, 1114111) || CharHelper.IsInInclusiveRange(utf32, 55296, 57343))
|
||||
{
|
||||
@@ -99,7 +98,7 @@ namespace Markdig.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
#region [ EntityMap ]
|
||||
#region [ EntityMap ]
|
||||
/// <summary>
|
||||
/// Source: http://www.w3.org/html/wg/drafts/html/master/syntax.html#named-character-references
|
||||
/// </summary>
|
||||
|
||||
293
src/Markdig/Helpers/FastStringWriter.cs
Normal file
293
src/Markdig/Helpers/FastStringWriter.cs
Normal file
@@ -0,0 +1,293 @@
|
||||
// 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;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Markdig.Helpers
|
||||
{
|
||||
internal sealed class FastStringWriter : TextWriter
|
||||
{
|
||||
#if NET452
|
||||
private static Task CompletedTask => Task.FromResult(0);
|
||||
#else
|
||||
private static Task CompletedTask => Task.CompletedTask;
|
||||
#endif
|
||||
|
||||
public override Encoding Encoding => Encoding.Unicode;
|
||||
|
||||
private char[] _chars;
|
||||
private int _pos;
|
||||
private string _newLine;
|
||||
|
||||
public FastStringWriter()
|
||||
{
|
||||
_chars = new char[1024];
|
||||
_newLine = "\n";
|
||||
}
|
||||
|
||||
[AllowNull]
|
||||
public override string NewLine
|
||||
{
|
||||
get => _newLine;
|
||||
set => _newLine = value ?? Environment.NewLine;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void Write(char value)
|
||||
{
|
||||
char[] chars = _chars;
|
||||
int pos = _pos;
|
||||
if ((uint)pos < (uint)chars.Length)
|
||||
{
|
||||
chars[pos] = value;
|
||||
_pos = pos + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
GrowAndAppend(value);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteLine(char value)
|
||||
{
|
||||
Write(value);
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
public override Task WriteAsync(char value)
|
||||
{
|
||||
Write(value);
|
||||
return CompletedTask;
|
||||
}
|
||||
|
||||
public override Task WriteLineAsync(char value)
|
||||
{
|
||||
WriteLine(value);
|
||||
return CompletedTask;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void Write(string? value)
|
||||
{
|
||||
if (value is not null)
|
||||
{
|
||||
if (_pos > _chars.Length - value.Length)
|
||||
{
|
||||
Grow(value.Length);
|
||||
}
|
||||
|
||||
value.AsSpan().CopyTo(_chars.AsSpan(_pos));
|
||||
_pos += value.Length;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteLine(string? value)
|
||||
{
|
||||
Write(value);
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
public override Task WriteAsync(string? value)
|
||||
{
|
||||
Write(value);
|
||||
return CompletedTask;
|
||||
}
|
||||
|
||||
public override Task WriteLineAsync(string? value)
|
||||
{
|
||||
WriteLine(value);
|
||||
return CompletedTask;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void Write(char[]? buffer)
|
||||
{
|
||||
if (buffer is not null)
|
||||
{
|
||||
if (_pos > _chars.Length - buffer.Length)
|
||||
{
|
||||
Grow(buffer.Length);
|
||||
}
|
||||
|
||||
buffer.CopyTo(_chars.AsSpan(_pos));
|
||||
_pos += buffer.Length;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteLine(char[]? buffer)
|
||||
{
|
||||
Write(buffer);
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void Write(char[] buffer, int index, int count)
|
||||
{
|
||||
if (buffer is not null)
|
||||
{
|
||||
if (_pos > _chars.Length - count)
|
||||
{
|
||||
Grow(buffer.Length);
|
||||
}
|
||||
|
||||
buffer.AsSpan(index, count).CopyTo(_chars.AsSpan(_pos));
|
||||
_pos += count;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteLine(char[] buffer, int index, int count)
|
||||
{
|
||||
Write(buffer, index, count);
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
public override Task WriteAsync(char[] buffer, int index, int count)
|
||||
{
|
||||
Write(buffer, index, count);
|
||||
return CompletedTask;
|
||||
}
|
||||
|
||||
public override Task WriteLineAsync(char[] buffer, int index, int count)
|
||||
{
|
||||
WriteLine(buffer, index, count);
|
||||
return CompletedTask;
|
||||
}
|
||||
|
||||
#if !(NET452 || NETSTANDARD2_0)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void Write(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (_pos > _chars.Length - value.Length)
|
||||
{
|
||||
Grow(value.Length);
|
||||
}
|
||||
|
||||
value.CopyTo(_chars.AsSpan(_pos));
|
||||
_pos += value.Length;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteLine(ReadOnlySpan<char> buffer)
|
||||
{
|
||||
Write(buffer);
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
public override Task WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Write(buffer.Span);
|
||||
return CompletedTask;
|
||||
}
|
||||
|
||||
public override Task WriteLineAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
WriteLine(buffer.Span);
|
||||
return CompletedTask;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !(NET452 || NETSTANDARD2_0 || NETSTANDARD2_1)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void Write(StringBuilder? value)
|
||||
{
|
||||
if (value is not null)
|
||||
{
|
||||
int length = value.Length;
|
||||
if (_pos > _chars.Length - length)
|
||||
{
|
||||
Grow(length);
|
||||
}
|
||||
|
||||
value.CopyTo(0, _chars.AsSpan(_pos), length);
|
||||
_pos += length;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteLine(StringBuilder? value)
|
||||
{
|
||||
Write(value);
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
public override Task WriteAsync(StringBuilder? value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Write(value);
|
||||
return CompletedTask;
|
||||
}
|
||||
|
||||
public override Task WriteLineAsync(StringBuilder? value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
WriteLine(value);
|
||||
return CompletedTask;
|
||||
}
|
||||
#endif
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteLine()
|
||||
{
|
||||
foreach (char c in _newLine)
|
||||
{
|
||||
Write(c);
|
||||
}
|
||||
}
|
||||
|
||||
public override Task WriteLineAsync()
|
||||
{
|
||||
WriteLine();
|
||||
return CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void GrowAndAppend(char value)
|
||||
{
|
||||
Grow(1);
|
||||
Write(value);
|
||||
}
|
||||
|
||||
private void Grow(int additionalCapacityBeyondPos)
|
||||
{
|
||||
Debug.Assert(additionalCapacityBeyondPos > 0);
|
||||
Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "No resize is needed.");
|
||||
|
||||
char[] newArray = new char[(int)Math.Max((uint)(_pos + additionalCapacityBeyondPos), (uint)_chars.Length * 2)];
|
||||
_chars.AsSpan(0, _pos).CopyTo(newArray);
|
||||
_chars = newArray;
|
||||
}
|
||||
|
||||
|
||||
public override void Flush() { }
|
||||
|
||||
public override void Close() { }
|
||||
|
||||
public override Task FlushAsync() => CompletedTask;
|
||||
|
||||
#if !(NET452 || NETSTANDARD2_0)
|
||||
public override ValueTask DisposeAsync() => default;
|
||||
#endif
|
||||
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_pos = 0;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _chars.AsSpan(0, _pos).ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
|
||||
namespace Markdig.Helpers
|
||||
{
|
||||
@@ -39,22 +38,22 @@ namespace Markdig.Helpers
|
||||
|
||||
public static bool TryParseHtmlTag(ref StringSlice text, [NotNullWhen(true)] out string? htmlTag)
|
||||
{
|
||||
var builder = StringBuilderCache.Local();
|
||||
if (TryParseHtmlTag(ref text, builder))
|
||||
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
if (TryParseHtmlTag(ref text, ref builder))
|
||||
{
|
||||
htmlTag = builder.GetStringAndReset();
|
||||
htmlTag = builder.ToString();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Dispose();
|
||||
htmlTag = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryParseHtmlTag(ref StringSlice text, StringBuilder builder)
|
||||
private static bool TryParseHtmlTag(ref StringSlice text, ref ValueStringBuilder builder)
|
||||
{
|
||||
if (builder is null) ThrowHelper.ArgumentNullException(nameof(builder));
|
||||
var c = text.CurrentChar;
|
||||
if (c != '<')
|
||||
{
|
||||
@@ -67,29 +66,29 @@ namespace Markdig.Helpers
|
||||
switch (c)
|
||||
{
|
||||
case '/':
|
||||
return TryParseHtmlCloseTag(ref text, builder);
|
||||
return TryParseHtmlCloseTag(ref text, ref builder);
|
||||
case '?':
|
||||
return TryParseHtmlTagProcessingInstruction(ref text, builder);
|
||||
return TryParseHtmlTagProcessingInstruction(ref text, ref builder);
|
||||
case '!':
|
||||
builder.Append(c);
|
||||
c = text.NextChar();
|
||||
if (c == '-')
|
||||
{
|
||||
return TryParseHtmlTagHtmlComment(ref text, builder);
|
||||
return TryParseHtmlTagHtmlComment(ref text, ref builder);
|
||||
}
|
||||
|
||||
if (c == '[')
|
||||
{
|
||||
return TryParseHtmlTagCData(ref text, builder);
|
||||
return TryParseHtmlTagCData(ref text, ref builder);
|
||||
}
|
||||
|
||||
return TryParseHtmlTagDeclaration(ref text, builder);
|
||||
return TryParseHtmlTagDeclaration(ref text, ref builder);
|
||||
}
|
||||
|
||||
return TryParseHtmlTagOpenTag(ref text, builder);
|
||||
return TryParseHtmlTagOpenTag(ref text, ref builder);
|
||||
}
|
||||
|
||||
internal static bool TryParseHtmlTagOpenTag(ref StringSlice text, StringBuilder builder)
|
||||
internal static bool TryParseHtmlTagOpenTag(ref StringSlice text, ref ValueStringBuilder builder)
|
||||
{
|
||||
var c = text.CurrentChar;
|
||||
|
||||
@@ -244,7 +243,7 @@ namespace Markdig.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseHtmlTagDeclaration(ref StringSlice text, StringBuilder builder)
|
||||
private static bool TryParseHtmlTagDeclaration(ref StringSlice text, ref ValueStringBuilder builder)
|
||||
{
|
||||
var c = text.CurrentChar;
|
||||
bool hasAlpha = false;
|
||||
@@ -279,7 +278,7 @@ namespace Markdig.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseHtmlTagCData(ref StringSlice text, StringBuilder builder)
|
||||
private static bool TryParseHtmlTagCData(ref StringSlice text, ref ValueStringBuilder builder)
|
||||
{
|
||||
if (text.Match("[CDATA["))
|
||||
{
|
||||
@@ -310,7 +309,7 @@ namespace Markdig.Helpers
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool TryParseHtmlCloseTag(ref StringSlice text, StringBuilder builder)
|
||||
internal static bool TryParseHtmlCloseTag(ref StringSlice text, ref ValueStringBuilder builder)
|
||||
{
|
||||
// </[A-Za-z][A-Za-z0-9]+\s*>
|
||||
builder.Append('/');
|
||||
@@ -355,7 +354,7 @@ namespace Markdig.Helpers
|
||||
}
|
||||
|
||||
|
||||
private static bool TryParseHtmlTagHtmlComment(ref StringSlice text, StringBuilder builder)
|
||||
private static bool TryParseHtmlTagHtmlComment(ref StringSlice text, ref ValueStringBuilder builder)
|
||||
{
|
||||
var c = text.NextChar();
|
||||
if (c != '-')
|
||||
@@ -393,7 +392,7 @@ namespace Markdig.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseHtmlTagProcessingInstruction(ref StringSlice text, StringBuilder builder)
|
||||
private static bool TryParseHtmlTagProcessingInstruction(ref StringSlice text, ref ValueStringBuilder builder)
|
||||
{
|
||||
builder.Append('?');
|
||||
var prevChar = '\0';
|
||||
@@ -435,13 +434,12 @@ namespace Markdig.Helpers
|
||||
// remove backslashes before punctuation chars:
|
||||
int searchPos = 0;
|
||||
int lastPos = 0;
|
||||
char c;
|
||||
char c = '\0';
|
||||
char[] search = removeBackSlash ? SearchBackAndAmp : SearchAmp;
|
||||
StringBuilder? sb = null;
|
||||
var sb = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
|
||||
while ((searchPos = text!.IndexOfAny(search, searchPos)) != -1)
|
||||
{
|
||||
sb ??= StringBuilderCache.Local();
|
||||
c = text[searchPos];
|
||||
if (removeBackSlash && c == '\\')
|
||||
{
|
||||
@@ -453,7 +451,7 @@ namespace Markdig.Helpers
|
||||
c = text[searchPos];
|
||||
if (c.IsEscapableSymbol())
|
||||
{
|
||||
sb.Append(text, lastPos, searchPos - lastPos - 1);
|
||||
sb.Append(text.AsSpan(lastPos, searchPos - lastPos - 1));
|
||||
lastPos = searchPos;
|
||||
}
|
||||
}
|
||||
@@ -473,26 +471,29 @@ namespace Markdig.Helpers
|
||||
var decoded = EntityHelper.DecodeEntity(text.AsSpan(entityNameStart, entityNameLength));
|
||||
if (decoded != null)
|
||||
{
|
||||
sb.Append(text, lastPos, searchPos - match - lastPos);
|
||||
sb.Append(text.AsSpan(lastPos, searchPos - match - lastPos));
|
||||
sb.Append(decoded);
|
||||
lastPos = searchPos;
|
||||
}
|
||||
}
|
||||
else if (numericEntity >= 0)
|
||||
{
|
||||
sb.Append(text, lastPos, searchPos - match - lastPos);
|
||||
EntityHelper.DecodeEntity(numericEntity, sb);
|
||||
sb.Append(text.AsSpan(lastPos, searchPos - match - lastPos));
|
||||
EntityHelper.DecodeEntity(numericEntity, ref sb);
|
||||
lastPos = searchPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sb is null || lastPos == 0)
|
||||
if (c == 0)
|
||||
{
|
||||
sb.Dispose();
|
||||
return text;
|
||||
}
|
||||
|
||||
sb.Append(text, lastPos, text.Length - lastPos);
|
||||
return sb.GetStringAndReset();
|
||||
sb.Append(text.AsSpan(lastPos, text.Length - lastPos));
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
44
src/Markdig/Helpers/LazySubstring.cs
Normal file
44
src/Markdig/Helpers/LazySubstring.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Markdig.Helpers
|
||||
{
|
||||
@@ -39,41 +41,55 @@ namespace Markdig.Helpers
|
||||
/// <returns>A new line or null if the end of <see cref="TextReader"/> has been reached</returns>
|
||||
public StringSlice ReadLine()
|
||||
{
|
||||
string text = _text;
|
||||
string? text = _text;
|
||||
int end = text.Length;
|
||||
int sourcePosition = SourcePosition;
|
||||
int newSourcePosition = int.MaxValue;
|
||||
NewLine newLine = NewLine.None;
|
||||
|
||||
for (int i = sourcePosition; i < text.Length; i++)
|
||||
if ((uint)sourcePosition >= (uint)end)
|
||||
{
|
||||
char c = text[i];
|
||||
if (c == '\r')
|
||||
text = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
ReadOnlySpan<char> span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), sourcePosition), end - sourcePosition);
|
||||
#else
|
||||
ReadOnlySpan<char> span = text.AsSpan(sourcePosition);
|
||||
#endif
|
||||
|
||||
int crlf = span.IndexOfAny('\r', '\n');
|
||||
if (crlf >= 0)
|
||||
{
|
||||
int length = 1;
|
||||
var newLine = NewLine.CarriageReturn;
|
||||
if (c == '\r' && (uint)(i + 1) < (uint)text.Length && text[i + 1] == '\n')
|
||||
end = sourcePosition + crlf;
|
||||
newSourcePosition = end + 1;
|
||||
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
if (Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), end) == '\r')
|
||||
#else
|
||||
if ((uint)end < (uint)text.Length && text[end] == '\r')
|
||||
#endif
|
||||
{
|
||||
i++;
|
||||
length = 2;
|
||||
newLine = NewLine.CarriageReturnLineFeed;
|
||||
if ((uint)(newSourcePosition) < (uint)text.Length && text[newSourcePosition] == '\n')
|
||||
{
|
||||
newLine = NewLine.CarriageReturnLineFeed;
|
||||
newSourcePosition++;
|
||||
}
|
||||
else
|
||||
{
|
||||
newLine = NewLine.CarriageReturn;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newLine = NewLine.LineFeed;
|
||||
}
|
||||
|
||||
var slice = new StringSlice(text, sourcePosition, i - length, newLine);
|
||||
SourcePosition = i + 1;
|
||||
return slice;
|
||||
}
|
||||
|
||||
if (c == '\n')
|
||||
{
|
||||
var slice = new StringSlice(text, sourcePosition, i - 1, NewLine.LineFeed);
|
||||
SourcePosition = i + 1;
|
||||
return slice;
|
||||
}
|
||||
}
|
||||
|
||||
if (sourcePosition >= text.Length)
|
||||
return default;
|
||||
|
||||
SourcePosition = int.MaxValue;
|
||||
return new StringSlice(text, sourcePosition, text.Length - 1);
|
||||
SourcePosition = newSourcePosition;
|
||||
return new StringSlice(text, sourcePosition, end - 1, newLine, dummy: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace Markdig.Helpers
|
||||
@@ -21,7 +20,7 @@ namespace Markdig.Helpers
|
||||
|
||||
public static string Urilize(string headingText, bool allowOnlyAscii, bool keepOpeningDigits = false)
|
||||
{
|
||||
var headingBuffer = StringBuilderCache.Local();
|
||||
var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
bool hasLetter = keepOpeningDigits && headingText.Length > 0 && char.IsLetterOrDigit(headingText[0]);
|
||||
bool previousIsSpace = false;
|
||||
for (int i = 0; i < headingText.Length; i++)
|
||||
@@ -92,15 +91,13 @@ namespace Markdig.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
var text = headingBuffer.ToString();
|
||||
headingBuffer.Length = 0;
|
||||
return text;
|
||||
return headingBuffer.ToString();
|
||||
}
|
||||
|
||||
public static string UrilizeAsGfm(string headingText)
|
||||
{
|
||||
// Following https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb
|
||||
var headingBuffer = StringBuilderCache.Local();
|
||||
var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
for (int i = 0; i < headingText.Length; i++)
|
||||
{
|
||||
var c = headingText[i];
|
||||
@@ -109,7 +106,7 @@ namespace Markdig.Helpers
|
||||
headingBuffer.Append(c == ' ' ? '-' : char.ToLowerInvariant(c));
|
||||
}
|
||||
}
|
||||
return headingBuffer.GetStringAndReset();
|
||||
return headingBuffer.ToString();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -165,7 +162,7 @@ namespace Markdig.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
var builder = StringBuilderCache.Local();
|
||||
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
|
||||
// ****************************
|
||||
// 1. Scan scheme or user email
|
||||
@@ -193,8 +190,7 @@ namespace Markdig.Helpers
|
||||
// a scheme is any sequence of 2–32 characters
|
||||
if (state > 0 && builder.Length >= 32)
|
||||
{
|
||||
builder.Length = 0;
|
||||
return false;
|
||||
goto ReturnFalse;
|
||||
}
|
||||
builder.Append(c);
|
||||
}
|
||||
@@ -202,8 +198,7 @@ namespace Markdig.Helpers
|
||||
{
|
||||
if (state < 0 || builder.Length <= 2)
|
||||
{
|
||||
builder.Length = 0;
|
||||
return false;
|
||||
goto ReturnFalse;
|
||||
}
|
||||
state = 1;
|
||||
break;
|
||||
@@ -211,16 +206,14 @@ namespace Markdig.Helpers
|
||||
{
|
||||
if (state > 0)
|
||||
{
|
||||
builder.Length = 0;
|
||||
return false;
|
||||
goto ReturnFalse;
|
||||
}
|
||||
state = -1;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Length = 0;
|
||||
return false;
|
||||
goto ReturnFalse;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +242,6 @@ namespace Markdig.Helpers
|
||||
|
||||
text.SkipChar();
|
||||
link = builder.ToString();
|
||||
builder.Length = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -297,7 +289,6 @@ namespace Markdig.Helpers
|
||||
{
|
||||
text.SkipChar();
|
||||
link = builder.ToString();
|
||||
builder.Length = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -318,7 +309,8 @@ namespace Markdig.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
builder.Length = 0;
|
||||
ReturnFalse:
|
||||
builder.Dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -528,8 +520,7 @@ namespace Markdig.Helpers
|
||||
|
||||
public static bool TryParseTitle<T>(ref T text, out string? title, out char enclosingCharacter) where T : ICharIterator
|
||||
{
|
||||
bool isValid = false;
|
||||
var buffer = StringBuilderCache.Local();
|
||||
var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
enclosingCharacter = '\0';
|
||||
|
||||
// a sequence of zero or more characters between straight double-quote characters ("), including a " character only if it is backslash-escaped, or
|
||||
@@ -582,8 +573,7 @@ namespace Markdig.Helpers
|
||||
|
||||
// Skip last quote
|
||||
text.SkipChar();
|
||||
isValid = true;
|
||||
break;
|
||||
goto ReturnValid;
|
||||
}
|
||||
|
||||
if (hasEscape && !c.IsAsciiPunctuation())
|
||||
@@ -615,15 +605,18 @@ namespace Markdig.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
title = isValid ? buffer.ToString() : null;
|
||||
buffer.Length = 0;
|
||||
return isValid;
|
||||
buffer.Dispose();
|
||||
title = null;
|
||||
return false;
|
||||
|
||||
ReturnValid:
|
||||
title = buffer.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseTitleTrivia<T>(ref T text, out string? title, out char enclosingCharacter) where T : ICharIterator
|
||||
{
|
||||
bool isValid = false;
|
||||
var buffer = StringBuilderCache.Local();
|
||||
var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
enclosingCharacter = '\0';
|
||||
|
||||
// a sequence of zero or more characters between straight double-quote characters ("), including a " character only if it is backslash-escaped, or
|
||||
@@ -676,8 +669,7 @@ namespace Markdig.Helpers
|
||||
|
||||
// Skip last quote
|
||||
text.SkipChar();
|
||||
isValid = true;
|
||||
break;
|
||||
goto ReturnValid;
|
||||
}
|
||||
|
||||
if (hasEscape && !c.IsAsciiPunctuation())
|
||||
@@ -709,9 +701,13 @@ namespace Markdig.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
title = isValid ? buffer.ToString() : null;
|
||||
buffer.Length = 0;
|
||||
return isValid;
|
||||
buffer.Dispose();
|
||||
title = null;
|
||||
return false;
|
||||
|
||||
ReturnValid:
|
||||
title = buffer.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseUrl<T>(T text, [NotNullWhen(true)] out string? link) where T : ICharIterator
|
||||
@@ -723,7 +719,7 @@ namespace Markdig.Helpers
|
||||
{
|
||||
bool isValid = false;
|
||||
hasPointyBrackets = false;
|
||||
var buffer = StringBuilderCache.Local();
|
||||
var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
|
||||
var c = text.CurrentChar;
|
||||
|
||||
@@ -854,8 +850,15 @@ namespace Markdig.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
link = isValid ? buffer.ToString() : null;
|
||||
buffer.Length = 0;
|
||||
if (isValid)
|
||||
{
|
||||
link = buffer.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.Dispose();
|
||||
link = null;
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
@@ -863,8 +866,7 @@ namespace Markdig.Helpers
|
||||
{
|
||||
bool isValid = false;
|
||||
hasPointyBrackets = false;
|
||||
var buffer = StringBuilderCache.Local();
|
||||
var unescaped = new StringBuilder();
|
||||
var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
|
||||
var c = text.CurrentChar;
|
||||
|
||||
@@ -892,13 +894,11 @@ namespace Markdig.Helpers
|
||||
if (hasEscape && !c.IsAsciiPunctuation())
|
||||
{
|
||||
buffer.Append('\\');
|
||||
unescaped.Append('\\');
|
||||
}
|
||||
|
||||
if (c == '\\')
|
||||
{
|
||||
hasEscape = true;
|
||||
unescaped.Append('\\');
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -910,7 +910,6 @@ namespace Markdig.Helpers
|
||||
hasEscape = false;
|
||||
|
||||
buffer.Append(c);
|
||||
unescaped.Append(c);
|
||||
|
||||
} while (c != '\0');
|
||||
}
|
||||
@@ -958,7 +957,6 @@ namespace Markdig.Helpers
|
||||
{
|
||||
hasEscape = true;
|
||||
c = text.NextChar();
|
||||
unescaped.Append('\\');
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -989,7 +987,6 @@ namespace Markdig.Helpers
|
||||
}
|
||||
|
||||
buffer.Append(c);
|
||||
unescaped.Append(c);
|
||||
|
||||
c = text.NextChar();
|
||||
}
|
||||
@@ -1000,8 +997,15 @@ namespace Markdig.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
link = isValid ? buffer.ToString() : null;
|
||||
buffer.Length = 0;
|
||||
if (isValid)
|
||||
{
|
||||
link = buffer.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.Dispose();
|
||||
link = null;
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
@@ -1357,7 +1361,7 @@ namespace Markdig.Helpers
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var buffer = StringBuilderCache.Local();
|
||||
var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
|
||||
var startLabel = -1;
|
||||
var endLabel = -1;
|
||||
@@ -1365,7 +1369,6 @@ namespace Markdig.Helpers
|
||||
bool hasEscape = false;
|
||||
bool previousWhitespace = true;
|
||||
bool hasNonWhiteSpace = false;
|
||||
bool isValid = false;
|
||||
while (true)
|
||||
{
|
||||
c = lines.NextChar();
|
||||
@@ -1413,9 +1416,7 @@ namespace Markdig.Helpers
|
||||
{
|
||||
labelSpan = SourceSpan.Empty;
|
||||
}
|
||||
|
||||
label = buffer.ToString();
|
||||
isValid = true;
|
||||
goto ReturnValid;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -1458,9 +1459,12 @@ namespace Markdig.Helpers
|
||||
previousWhitespace = isWhitespace;
|
||||
}
|
||||
|
||||
buffer.Length = 0;
|
||||
buffer.Dispose();
|
||||
return false;
|
||||
|
||||
return isValid;
|
||||
ReturnValid:
|
||||
label = buffer.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseLabelTrivia<T>(ref T lines, bool allowEmpty, out string? label, out SourceSpan labelSpan) where T : ICharIterator
|
||||
@@ -1472,7 +1476,7 @@ namespace Markdig.Helpers
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var buffer = StringBuilderCache.Local();
|
||||
var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
|
||||
var startLabel = -1;
|
||||
var endLabel = -1;
|
||||
@@ -1480,7 +1484,6 @@ namespace Markdig.Helpers
|
||||
bool hasEscape = false;
|
||||
bool previousWhitespace = true;
|
||||
bool hasNonWhiteSpace = false;
|
||||
bool isValid = false;
|
||||
while (true)
|
||||
{
|
||||
c = lines.NextChar();
|
||||
@@ -1528,9 +1531,7 @@ namespace Markdig.Helpers
|
||||
{
|
||||
labelSpan = SourceSpan.Empty;
|
||||
}
|
||||
|
||||
label = buffer.ToString();
|
||||
isValid = true;
|
||||
goto ReturnValid;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -1577,10 +1578,12 @@ namespace Markdig.Helpers
|
||||
previousWhitespace = isWhitespace;
|
||||
}
|
||||
|
||||
buffer.Length = 0;
|
||||
buffer.Dispose();
|
||||
return false;
|
||||
|
||||
return isValid;
|
||||
ReturnValid:
|
||||
label = buffer.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,5 @@ namespace Markdig.Helpers
|
||||
{
|
||||
return builder.Append(slice.Text, slice.Start, slice.Length);
|
||||
}
|
||||
|
||||
internal static string GetStringAndReset(this StringBuilder builder)
|
||||
{
|
||||
string text = builder.ToString();
|
||||
builder.Length = 0;
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,14 @@ namespace Markdig.Helpers
|
||||
Count--;
|
||||
}
|
||||
|
||||
internal void RemoveStartRange(int toRemove)
|
||||
{
|
||||
int remaining = Count - toRemove;
|
||||
Count = remaining;
|
||||
Array.Copy(Lines, toRemove, Lines, 0, remaining);
|
||||
Array.Clear(Lines, remaining, toRemove);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified line to this instance.
|
||||
/// </summary>
|
||||
@@ -139,7 +147,7 @@ namespace Markdig.Helpers
|
||||
}
|
||||
|
||||
// Else use a builder
|
||||
var builder = StringBuilderCache.Local();
|
||||
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
int previousStartOfLine = 0;
|
||||
var newLine = NewLine.None;
|
||||
for (int i = 0; i < Count; i++)
|
||||
@@ -152,13 +160,13 @@ namespace Markdig.Helpers
|
||||
ref StringLine line = ref Lines[i];
|
||||
if (!line.Slice.IsEmpty)
|
||||
{
|
||||
builder.Append(line.Slice.Text, line.Slice.Start, line.Slice.Length);
|
||||
builder.Append(line.Slice.AsSpan());
|
||||
}
|
||||
newLine = line.NewLine;
|
||||
|
||||
lineOffsets?.Add(new LineOffset(line.Position, line.Column, line.Slice.Start - line.Position, previousStartOfLine, builder.Length));
|
||||
}
|
||||
return new StringSlice(builder.GetStringAndReset());
|
||||
return new StringSlice(builder.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -213,21 +221,24 @@ namespace Markdig.Helpers
|
||||
public struct Iterator : ICharIterator
|
||||
{
|
||||
private readonly StringLineGroup _lines;
|
||||
private StringSlice _currentSlice;
|
||||
private int _offset;
|
||||
|
||||
public Iterator(StringLineGroup lines)
|
||||
public Iterator(StringLineGroup stringLineGroup)
|
||||
{
|
||||
this._lines = lines;
|
||||
_lines = stringLineGroup;
|
||||
Start = -1;
|
||||
_offset = -1;
|
||||
SliceIndex = 0;
|
||||
CurrentChar = '\0';
|
||||
End = -1;
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
StringLine[] lines = stringLineGroup.Lines;
|
||||
for (int i = 0; i < stringLineGroup.Count && i < lines.Length; i++)
|
||||
{
|
||||
ref StringLine line = ref lines.Lines[i];
|
||||
End += line.Slice.Length + line.NewLine.Length(); // Add chars
|
||||
ref StringSlice slice = ref lines[i].Slice;
|
||||
End += slice.Length + slice.NewLine.Length(); // Add chars
|
||||
}
|
||||
_currentSlice = _lines.Lines[0].Slice;
|
||||
SkipChar();
|
||||
}
|
||||
|
||||
@@ -243,17 +254,14 @@ namespace Markdig.Helpers
|
||||
|
||||
public StringLineGroup Remaining()
|
||||
{
|
||||
var lines = _lines;
|
||||
StringLineGroup lines = _lines;
|
||||
if (IsEmpty)
|
||||
{
|
||||
lines.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = SliceIndex - 1; i >= 0; i--)
|
||||
{
|
||||
lines.RemoveAt(i);
|
||||
}
|
||||
lines.RemoveStartRange(SliceIndex);
|
||||
|
||||
if (lines.Count > 0 && _offset > 0)
|
||||
{
|
||||
@@ -266,59 +274,85 @@ namespace Markdig.Helpers
|
||||
return lines;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public char NextChar()
|
||||
{
|
||||
Start++;
|
||||
if (Start <= End)
|
||||
{
|
||||
var slice = _lines.Lines[SliceIndex].Slice;
|
||||
ref StringSlice slice = ref _currentSlice;
|
||||
_offset++;
|
||||
if (_offset < slice.Length)
|
||||
|
||||
int index = slice.Start + _offset;
|
||||
string text = slice.Text;
|
||||
if (index <= slice.End && (uint)index < (uint)text.Length)
|
||||
{
|
||||
CurrentChar = slice[slice.Start + _offset];
|
||||
char c = text[index];
|
||||
CurrentChar = c;
|
||||
return c;
|
||||
}
|
||||
else
|
||||
{
|
||||
var newLine = slice.NewLine;
|
||||
if (_offset == slice.Length)
|
||||
{
|
||||
if (newLine == NewLine.LineFeed)
|
||||
{
|
||||
CurrentChar = '\n';
|
||||
SliceIndex++;
|
||||
_offset = -1;
|
||||
}
|
||||
else if (newLine == NewLine.CarriageReturn)
|
||||
{
|
||||
CurrentChar = '\r';
|
||||
SliceIndex++;
|
||||
_offset = -1;
|
||||
}
|
||||
else if (newLine == NewLine.CarriageReturnLineFeed)
|
||||
{
|
||||
CurrentChar = '\r';
|
||||
}
|
||||
}
|
||||
else if (_offset - 1 == slice.Length)
|
||||
{
|
||||
if (newLine == NewLine.CarriageReturnLineFeed)
|
||||
{
|
||||
CurrentChar = '\n';
|
||||
SliceIndex++;
|
||||
_offset = -1;
|
||||
}
|
||||
}
|
||||
return NextCharNewLine();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentChar = '\0';
|
||||
Start = End + 1;
|
||||
SliceIndex = _lines.Count;
|
||||
return NextCharEndOfEnumerator();
|
||||
}
|
||||
}
|
||||
|
||||
private char NextCharNewLine()
|
||||
{
|
||||
int sliceLength = _currentSlice.Length;
|
||||
NewLine newLine = _currentSlice.NewLine;
|
||||
|
||||
if (_offset == sliceLength)
|
||||
{
|
||||
if (newLine == NewLine.LineFeed)
|
||||
{
|
||||
CurrentChar = '\n';
|
||||
goto MoveToNewLine;
|
||||
}
|
||||
else if (newLine == NewLine.CarriageReturn)
|
||||
{
|
||||
CurrentChar = '\r';
|
||||
goto MoveToNewLine;
|
||||
}
|
||||
else if (newLine == NewLine.CarriageReturnLineFeed)
|
||||
{
|
||||
CurrentChar = '\r';
|
||||
}
|
||||
}
|
||||
else if (_offset - 1 == sliceLength)
|
||||
{
|
||||
if (newLine == NewLine.CarriageReturnLineFeed)
|
||||
{
|
||||
CurrentChar = '\n';
|
||||
goto MoveToNewLine;
|
||||
}
|
||||
}
|
||||
|
||||
goto Return;
|
||||
|
||||
MoveToNewLine:
|
||||
SliceIndex++;
|
||||
_offset = -1;
|
||||
_currentSlice = _lines.Lines[SliceIndex];
|
||||
|
||||
Return:
|
||||
return CurrentChar;
|
||||
}
|
||||
|
||||
private char NextCharEndOfEnumerator()
|
||||
{
|
||||
CurrentChar = '\0';
|
||||
Start = End + 1;
|
||||
SliceIndex = _lines.Count;
|
||||
return '\0';
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SkipChar() => NextChar();
|
||||
|
||||
public readonly char PeekChar() => PeekChar(1);
|
||||
@@ -335,16 +369,17 @@ namespace Markdig.Helpers
|
||||
offset += _offset;
|
||||
|
||||
int sliceIndex = SliceIndex;
|
||||
ref StringLine line = ref _lines.Lines[sliceIndex];
|
||||
ref StringSlice slice = ref line.Slice;
|
||||
if (!(line.NewLine == NewLine.CarriageReturnLineFeed && offset == slice.Length + 1))
|
||||
ref StringSlice slice = ref _lines.Lines[sliceIndex].Slice;
|
||||
NewLine newLine = slice.NewLine;
|
||||
|
||||
if (!(newLine == NewLine.CarriageReturnLineFeed && offset == slice.Length + 1))
|
||||
{
|
||||
while (offset > slice.Length)
|
||||
{
|
||||
// We are not peeking at the same line
|
||||
offset -= slice.Length + 1; // + 1 for new line
|
||||
|
||||
Debug.Assert(sliceIndex + 1 < _lines.Lines.Length, "'Start + offset > End' check above should prevent us from indexing out of range");
|
||||
Debug.Assert(sliceIndex + 1 < _lines.Count, "'Start + offset > End' check above should prevent us from indexing out of range");
|
||||
slice = ref _lines.Lines[++sliceIndex].Slice;
|
||||
}
|
||||
}
|
||||
@@ -358,15 +393,15 @@ namespace Markdig.Helpers
|
||||
|
||||
if (offset == slice.Length)
|
||||
{
|
||||
if (line.NewLine == NewLine.LineFeed)
|
||||
if (newLine == NewLine.LineFeed)
|
||||
{
|
||||
return '\n';
|
||||
}
|
||||
if (line.NewLine == NewLine.CarriageReturn)
|
||||
if (newLine == NewLine.CarriageReturn)
|
||||
{
|
||||
return '\r';
|
||||
}
|
||||
if (line.NewLine == NewLine.CarriageReturnLineFeed)
|
||||
if (newLine == NewLine.CarriageReturnLineFeed)
|
||||
{
|
||||
return '\r'; // /r of /r/n (first character)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Markdig.Helpers
|
||||
{
|
||||
@@ -82,6 +83,15 @@ namespace Markdig.Helpers
|
||||
NewLine = newLine;
|
||||
}
|
||||
|
||||
// Internal ctor to skip the null check
|
||||
internal StringSlice(string text, int start, int end, NewLine newLine, bool dummy)
|
||||
{
|
||||
Text = text;
|
||||
Start = start;
|
||||
End = end;
|
||||
NewLine = newLine;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The text of this slice.
|
||||
/// </summary>
|
||||
@@ -453,6 +463,25 @@ namespace Markdig.Helpers
|
||||
return text.Substring(start, length);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly ReadOnlySpan<char> AsSpan()
|
||||
{
|
||||
string text = Text;
|
||||
int start = Start;
|
||||
int length = End - start + 1;
|
||||
|
||||
if (text is null || (ulong)(uint)start + (ulong)(uint)length > (ulong)(uint)text.Length)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), start), length);
|
||||
#else
|
||||
return text.AsSpan(start, length);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this slice is empty or made only of whitespaces.
|
||||
/// </summary>
|
||||
|
||||
130
src/Markdig/Helpers/TransformedStringCache.cs
Normal file
130
src/Markdig/Helpers/TransformedStringCache.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
// 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.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Markdig.Helpers
|
||||
{
|
||||
internal sealed class TransformedStringCache
|
||||
{
|
||||
internal const int InputLengthLimit = 20; // Avoid caching unreasonably long strings
|
||||
internal const int MaxEntriesPerCharacter = 8; // Avoid growing too much
|
||||
|
||||
private readonly EntryGroup[] _groups; // One per ASCII character
|
||||
private readonly Func<string, string> _transformation;
|
||||
|
||||
public TransformedStringCache(Func<string, string> transformation)
|
||||
{
|
||||
_transformation = transformation ?? throw new ArgumentNullException(nameof(transformation));
|
||||
_groups = new EntryGroup[128];
|
||||
}
|
||||
|
||||
public string Get(ReadOnlySpan<char> inputSpan)
|
||||
{
|
||||
if ((uint)(inputSpan.Length - 1) < InputLengthLimit) // Length: [1, LengthLimit]
|
||||
{
|
||||
int firstCharacter = inputSpan[0];
|
||||
EntryGroup[] groups = _groups;
|
||||
if ((uint)firstCharacter < (uint)groups.Length)
|
||||
{
|
||||
ref EntryGroup group = ref groups[firstCharacter];
|
||||
string? transformed = group.TryGet(inputSpan);
|
||||
if (transformed is null)
|
||||
{
|
||||
string input = inputSpan.ToString();
|
||||
transformed = _transformation(input);
|
||||
group.TryAdd(input, transformed);
|
||||
}
|
||||
return transformed;
|
||||
}
|
||||
}
|
||||
|
||||
return _transformation(inputSpan.ToString());
|
||||
}
|
||||
|
||||
public string Get(string input)
|
||||
{
|
||||
if ((uint)(input.Length - 1) < InputLengthLimit) // Length: [1, LengthLimit]
|
||||
{
|
||||
int firstCharacter = input[0];
|
||||
EntryGroup[] groups = _groups;
|
||||
if ((uint)firstCharacter < (uint)groups.Length)
|
||||
{
|
||||
ref EntryGroup group = ref groups[firstCharacter];
|
||||
string? transformed = group.TryGet(input.AsSpan());
|
||||
if (transformed is null)
|
||||
{
|
||||
transformed = _transformation(input);
|
||||
group.TryAdd(input, transformed);
|
||||
}
|
||||
return transformed;
|
||||
}
|
||||
}
|
||||
|
||||
return _transformation(input);
|
||||
}
|
||||
|
||||
private struct EntryGroup
|
||||
{
|
||||
private struct Entry
|
||||
{
|
||||
public string Input;
|
||||
public string Transformed;
|
||||
}
|
||||
|
||||
private Entry[]? _entries;
|
||||
|
||||
public string? TryGet(ReadOnlySpan<char> inputSpan)
|
||||
{
|
||||
Entry[]? entries = _entries;
|
||||
if (entries is not null)
|
||||
{
|
||||
for (int i = 0; i < entries.Length; i++)
|
||||
{
|
||||
if (inputSpan.SequenceEqual(entries[i].Input.AsSpan()))
|
||||
{
|
||||
return entries[i].Transformed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void TryAdd(string input, string transformed)
|
||||
{
|
||||
if (_entries is null)
|
||||
{
|
||||
Interlocked.CompareExchange(ref _entries, new Entry[MaxEntriesPerCharacter], null);
|
||||
}
|
||||
|
||||
if (_entries[MaxEntriesPerCharacter - 1].Input is null) // There is still space
|
||||
{
|
||||
lock (_entries)
|
||||
{
|
||||
for (int i = 0; i < _entries.Length; i++)
|
||||
{
|
||||
string? existingInput = _entries[i].Input;
|
||||
|
||||
if (existingInput is null)
|
||||
{
|
||||
ref Entry entry = ref _entries[i];
|
||||
Volatile.Write(ref entry.Transformed, transformed);
|
||||
Volatile.Write(ref entry.Input, input);
|
||||
break;
|
||||
}
|
||||
|
||||
if (input == existingInput)
|
||||
{
|
||||
// We lost a race and a different thread already added the same value
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
197
src/Markdig/Helpers/ValueStringBuilder.cs
Normal file
197
src/Markdig/Helpers/ValueStringBuilder.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
// 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.
|
||||
|
||||
// Inspired by https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Text/ValueStringBuilder.cs
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Markdig.Helpers
|
||||
{
|
||||
internal ref partial struct ValueStringBuilder
|
||||
{
|
||||
#if DEBUG
|
||||
public const int StackallocThreshold = 7;
|
||||
#else
|
||||
#if NET5_0_OR_GREATER
|
||||
// NET5+ has SkipLocalsInit, so allocating more is "free"
|
||||
public const int StackallocThreshold = 256;
|
||||
#else
|
||||
public const int StackallocThreshold = 64;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
private char[]? _arrayToReturnToPool;
|
||||
private Span<char> _chars;
|
||||
private int _pos;
|
||||
|
||||
public ValueStringBuilder(Span<char> initialBuffer)
|
||||
{
|
||||
_arrayToReturnToPool = null;
|
||||
_chars = initialBuffer;
|
||||
_pos = 0;
|
||||
}
|
||||
|
||||
public int Length
|
||||
{
|
||||
get => _pos;
|
||||
set
|
||||
{
|
||||
Debug.Assert(value >= 0);
|
||||
Debug.Assert(value <= _chars.Length);
|
||||
_pos = value;
|
||||
}
|
||||
}
|
||||
|
||||
public ref char this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
Debug.Assert(index < _pos);
|
||||
return ref _chars[index];
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string s = _chars.Slice(0, _pos).ToString();
|
||||
Dispose();
|
||||
return s;
|
||||
}
|
||||
|
||||
public ReadOnlySpan<char> AsSpan() => _chars.Slice(0, _pos);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Append(char c)
|
||||
{
|
||||
int pos = _pos;
|
||||
Span<char> chars = _chars;
|
||||
if ((uint)pos < (uint)chars.Length)
|
||||
{
|
||||
chars[pos] = c;
|
||||
_pos = pos + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
GrowAndAppend(c);
|
||||
}
|
||||
}
|
||||
|
||||
public void Append(char c, int count)
|
||||
{
|
||||
if (_pos > _chars.Length - count)
|
||||
{
|
||||
Grow(count);
|
||||
}
|
||||
|
||||
Span<char> dst = _chars.Slice(_pos, count);
|
||||
for (int i = 0; i < dst.Length; i++)
|
||||
{
|
||||
dst[i] = c;
|
||||
}
|
||||
_pos += count;
|
||||
}
|
||||
|
||||
public void Append(uint i)
|
||||
{
|
||||
if (i < 10)
|
||||
{
|
||||
Append((char)('0' + i));
|
||||
}
|
||||
else
|
||||
{
|
||||
Append(i.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Append(string s)
|
||||
{
|
||||
int pos = _pos;
|
||||
if (pos > _chars.Length - s.Length)
|
||||
{
|
||||
Grow(s.Length);
|
||||
}
|
||||
|
||||
s
|
||||
#if !NET5_0_OR_GREATER
|
||||
.AsSpan()
|
||||
#endif
|
||||
.CopyTo(_chars.Slice(pos));
|
||||
|
||||
_pos += s.Length;
|
||||
}
|
||||
|
||||
public void Append(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (_pos > _chars.Length - value.Length)
|
||||
{
|
||||
Grow(value.Length);
|
||||
}
|
||||
|
||||
value.CopyTo(_chars.Slice(_pos));
|
||||
_pos += value.Length;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<char> AppendSpan(int length)
|
||||
{
|
||||
int origPos = _pos;
|
||||
if (origPos > _chars.Length - length)
|
||||
{
|
||||
Grow(length);
|
||||
}
|
||||
|
||||
_pos = origPos + length;
|
||||
return _chars.Slice(origPos, length);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void GrowAndAppend(char c)
|
||||
{
|
||||
Grow(1);
|
||||
Append(c);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resize the internal buffer either by doubling current buffer size or
|
||||
/// by adding <paramref name="additionalCapacityBeyondPos"/> to
|
||||
/// <see cref="_pos"/> whichever is greater.
|
||||
/// </summary>
|
||||
/// <param name="additionalCapacityBeyondPos">
|
||||
/// Number of chars requested beyond current position.
|
||||
/// </param>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void Grow(int additionalCapacityBeyondPos)
|
||||
{
|
||||
Debug.Assert(additionalCapacityBeyondPos > 0);
|
||||
Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed.");
|
||||
|
||||
// Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative
|
||||
char[] poolArray = ArrayPool<char>.Shared.Rent((int)Math.Max((uint)(_pos + additionalCapacityBeyondPos), (uint)_chars.Length * 2));
|
||||
|
||||
_chars.Slice(0, _pos).CopyTo(poolArray);
|
||||
|
||||
char[]? toReturn = _arrayToReturnToPool;
|
||||
_chars = _arrayToReturnToPool = poolArray;
|
||||
if (toReturn != null)
|
||||
{
|
||||
ArrayPool<char>.Shared.Return(toReturn);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Dispose()
|
||||
{
|
||||
char[]? toReturn = _arrayToReturnToPool;
|
||||
this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
|
||||
if (toReturn != null)
|
||||
{
|
||||
ArrayPool<char>.Shared.Return(toReturn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,11 @@
|
||||
<None Remove="readme.md" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="readme.md" LunetApiDotNet="true"/>
|
||||
<AdditionalFiles Include="readme.md" LunetApiDotNet="true" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Markdig.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -37,11 +37,19 @@
|
||||
<ItemGroup>
|
||||
<None Include="../../img/markdig.png" Pack="true" PackagePath="" />
|
||||
<None Include="../../readme.md" Pack="true" PackagePath="/"/>
|
||||
<PackageReference Include="MinVer" Version="2.5.0">
|
||||
<PackageReference Include="MinVer" Version="3.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.*" PrivateAssets="All"/>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PatchVersion" AfterTargets="MinVer">
|
||||
<PropertyGroup>
|
||||
<!--In Markdig, the minor version is like a major version because Major is 0
|
||||
Need to remove this when Markdig will be >= 1.0-->
|
||||
<AssemblyVersion>$(MinVerMajor).$(MinVerMinor).0.0</AssemblyVersion>
|
||||
</PropertyGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -132,6 +132,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>
|
||||
|
||||
@@ -94,9 +94,7 @@ namespace Markdig
|
||||
|
||||
internal sealed class HtmlRendererCache : ObjectCache<HtmlRenderer>
|
||||
{
|
||||
private const int InitialCapacity = 1024;
|
||||
|
||||
private static readonly StringWriter _dummyWriter = new();
|
||||
private static readonly TextWriter s_dummyWriter = new FastStringWriter();
|
||||
|
||||
private readonly MarkdownPipeline _pipeline;
|
||||
private readonly bool _customWriter;
|
||||
@@ -109,7 +107,7 @@ namespace Markdig
|
||||
|
||||
protected override HtmlRenderer NewInstance()
|
||||
{
|
||||
var writer = _customWriter ? _dummyWriter : new StringWriter(new StringBuilder(InitialCapacity));
|
||||
TextWriter writer = _customWriter ? s_dummyWriter : new FastStringWriter();
|
||||
var renderer = new HtmlRenderer(writer);
|
||||
_pipeline.Setup(renderer);
|
||||
return renderer;
|
||||
@@ -121,11 +119,11 @@ namespace Markdig
|
||||
|
||||
if (_customWriter)
|
||||
{
|
||||
instance.Writer = _dummyWriter;
|
||||
instance.Writer = s_dummyWriter;
|
||||
}
|
||||
else
|
||||
{
|
||||
((StringWriter)instance.Writer).GetStringBuilder().Length = 0;
|
||||
((FastStringWriter)instance.Writer).Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Syntax;
|
||||
|
||||
@@ -74,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.
|
||||
@@ -144,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; }
|
||||
|
||||
@@ -445,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--)
|
||||
{
|
||||
@@ -464,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);
|
||||
@@ -509,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)
|
||||
{
|
||||
@@ -517,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
|
||||
@@ -541,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)
|
||||
@@ -582,7 +591,7 @@ namespace Markdig.Parsers
|
||||
{
|
||||
for (int i = 1; i < OpenedBlocks.Count; i++)
|
||||
{
|
||||
OpenedBlocks[i].IsOpen = true;
|
||||
OpenedBlocks[i].Block.IsOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,24 +601,27 @@ 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)
|
||||
{
|
||||
currentStackIndex = stackIndex < 0 ? OpenedBlocks.Count - 1 : stackIndex;
|
||||
CurrentBlock = null;
|
||||
LastBlock = null;
|
||||
for (int i = OpenedBlocks.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var block = OpenedBlocks[i];
|
||||
if (CurrentBlock is null)
|
||||
{
|
||||
CurrentBlock = block;
|
||||
}
|
||||
List<BlockWrapper> openedBlocks = OpenedBlocks;
|
||||
currentStackIndex = stackIndex < 0 ? openedBlocks.Count - 1 : stackIndex;
|
||||
|
||||
if (block is ContainerBlock container)
|
||||
Block? currentBlock = null;
|
||||
for (int i = openedBlocks.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var block = openedBlocks[i].Block;
|
||||
currentBlock ??= block;
|
||||
|
||||
if (block.IsContainerBlock)
|
||||
{
|
||||
CurrentContainer = container;
|
||||
LastBlock = CurrentContainer.LastChild;
|
||||
break;
|
||||
var currentContainer = Unsafe.As<ContainerBlock>(block);
|
||||
CurrentContainer = currentContainer;
|
||||
LastBlock = currentContainer.LastChild;
|
||||
CurrentBlock = currentBlock;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CurrentBlock = currentBlock;
|
||||
LastBlock = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -628,18 +640,18 @@ 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();
|
||||
|
||||
// If we have a paragraph block, we want to try to match other blocks before trying the Paragraph
|
||||
if (block is ParagraphBlock)
|
||||
if (block.IsParagraphBlock)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -675,7 +687,7 @@ namespace Markdig.Parsers
|
||||
}
|
||||
|
||||
// If we have a leaf block
|
||||
if (block is LeafBlock leaf && NewBlocks.Count == 0)
|
||||
if (block.IsLeafBlock && NewBlocks.Count == 0)
|
||||
{
|
||||
ContinueProcessingLine = false;
|
||||
if (!result.IsDiscard())
|
||||
@@ -689,7 +701,8 @@ namespace Markdig.Parsers
|
||||
UnwindAllIndents();
|
||||
}
|
||||
}
|
||||
leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia);
|
||||
|
||||
Unsafe.As<LeafBlock>(block).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -804,7 +817,7 @@ namespace Markdig.Parsers
|
||||
continue;
|
||||
}
|
||||
|
||||
IsLazy = blockParser is ParagraphBlockParser && lastBlock is ParagraphBlock;
|
||||
IsLazy = lastBlock.IsParagraphBlock && blockParser is ParagraphBlockParser;
|
||||
|
||||
var result = IsLazy
|
||||
? blockParser.TryContinue(this, lastBlock)
|
||||
@@ -825,7 +838,7 @@ namespace Markdig.Parsers
|
||||
// Special case for paragraph
|
||||
UpdateLastBlockAndContainer();
|
||||
|
||||
if (IsLazy && CurrentBlock is ParagraphBlock paragraph)
|
||||
if (IsLazy && CurrentBlock is { } currentBlock && currentBlock.IsParagraphBlock)
|
||||
{
|
||||
Debug.Assert(NewBlocks.Count == 0);
|
||||
|
||||
@@ -835,12 +848,13 @@ namespace Markdig.Parsers
|
||||
{
|
||||
UnwindAllIndents();
|
||||
}
|
||||
paragraph.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia);
|
||||
|
||||
Unsafe.As<ParagraphBlock>(currentBlock).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia);
|
||||
}
|
||||
if (TrackTrivia)
|
||||
{
|
||||
// special case: take care when refactoring this
|
||||
if (paragraph.Parent is QuoteBlock qb)
|
||||
if (currentBlock.Parent is QuoteBlock qb)
|
||||
{
|
||||
var triviaAfter = UseTrivia(Start - 1);
|
||||
qb.QuoteLines.Last().TriviaAfter = triviaAfter;
|
||||
@@ -893,20 +907,19 @@ namespace Markdig.Parsers
|
||||
block.Line = LineIndex;
|
||||
|
||||
// If we have a leaf block
|
||||
var leaf = block as LeafBlock;
|
||||
if (leaf != null)
|
||||
if (block.IsLeafBlock)
|
||||
{
|
||||
if (!result.IsDiscard())
|
||||
{
|
||||
if (TrackTrivia)
|
||||
{
|
||||
if (block is ParagraphBlock ||
|
||||
block is HtmlBlock)
|
||||
if (block.IsParagraphBlock || block is HtmlBlock)
|
||||
{
|
||||
UnwindAllIndents();
|
||||
}
|
||||
}
|
||||
leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia);
|
||||
|
||||
Unsafe.As<LeafBlock>(block).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia);
|
||||
}
|
||||
|
||||
if (newBlocks.Count > 0)
|
||||
@@ -934,7 +947,7 @@ namespace Markdig.Parsers
|
||||
// Add a block BlockProcessor to the stack (and leave it opened)
|
||||
OpenedBlocks.Add(block);
|
||||
|
||||
if (leaf != null)
|
||||
if (block.IsLeafBlock)
|
||||
{
|
||||
ContinueProcessingLine = false;
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
using System.Diagnostics;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
@@ -40,6 +40,9 @@ namespace Markdig.Parsers
|
||||
/// <seealso cref="BlockParser" />
|
||||
public abstract class FencedBlockParserBase<T> : FencedBlockParserBase where T : Block, IFencedBlock
|
||||
{
|
||||
private static readonly TransformedStringCache _infoStringCache = new(static infoString => HtmlHelper.Unescape(infoString));
|
||||
private TransformedStringCache? _infoPrefixCache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FencedBlockParserBase{T}"/> class.
|
||||
/// </summary>
|
||||
@@ -50,10 +53,22 @@ namespace Markdig.Parsers
|
||||
MaximumMatchCount = int.MaxValue;
|
||||
}
|
||||
|
||||
private string? _infoPrefix;
|
||||
/// <summary>
|
||||
/// Gets or sets the language prefix (default is "language-")
|
||||
/// </summary>
|
||||
public string? InfoPrefix { get; set; }
|
||||
public string? InfoPrefix
|
||||
{
|
||||
get => _infoPrefix;
|
||||
set
|
||||
{
|
||||
if (_infoPrefix != value)
|
||||
{
|
||||
_infoPrefixCache = new TransformedStringCache(infoString => value + infoString);
|
||||
_infoPrefix = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int MinimumMatchCount { get; set; }
|
||||
|
||||
@@ -161,7 +176,7 @@ namespace Markdig.Parsers
|
||||
|
||||
end:
|
||||
fenced.TriviaAfterFencedChar = afterFence;
|
||||
fenced.Info = HtmlHelper.Unescape(info.ToString());
|
||||
fenced.Info = _infoStringCache.Get(info.AsSpan());
|
||||
fenced.UnescapedInfo = info;
|
||||
fenced.TriviaAfterInfo = afterInfo;
|
||||
fenced.Arguments = HtmlHelper.Unescape(arg.ToString());
|
||||
@@ -182,9 +197,6 @@ namespace Markdig.Parsers
|
||||
/// <returns><c>true</c> if parsing of the line is successfull; <c>false</c> otherwise</returns>
|
||||
public static bool DefaultInfoParser(BlockProcessor state, ref StringSlice line, IFencedBlock fenced, char openingCharacter)
|
||||
{
|
||||
string infoString;
|
||||
string? argString = null;
|
||||
|
||||
// An info string cannot contain any backticks (unless it is a tilde block)
|
||||
int firstSpace = -1;
|
||||
if (openingCharacter == '`')
|
||||
@@ -215,9 +227,12 @@ namespace Markdig.Parsers
|
||||
}
|
||||
}
|
||||
|
||||
StringSlice infoStringSlice;
|
||||
string? argString = null;
|
||||
|
||||
if (firstSpace > 0)
|
||||
{
|
||||
infoString = line.Text.AsSpan(line.Start, firstSpace - line.Start).Trim().ToString();
|
||||
infoStringSlice = new StringSlice(line.Text, line.Start, firstSpace - 1);
|
||||
|
||||
// Skip any spaces after info string
|
||||
firstSpace++;
|
||||
@@ -234,16 +249,18 @@ namespace Markdig.Parsers
|
||||
}
|
||||
}
|
||||
|
||||
argString = line.Text.Substring(firstSpace, line.End - firstSpace + 1).Trim();
|
||||
var argStringSlice = new StringSlice(line.Text, firstSpace, line.End);
|
||||
argStringSlice.Trim();
|
||||
argString = argStringSlice.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
var lineCopy = line;
|
||||
lineCopy.Trim();
|
||||
infoString = lineCopy.ToString();
|
||||
infoStringSlice = line;
|
||||
}
|
||||
|
||||
fenced.Info = HtmlHelper.Unescape(infoString);
|
||||
infoStringSlice.Trim();
|
||||
|
||||
fenced.Info = _infoStringCache.Get(infoStringSlice.AsSpan());
|
||||
fenced.Arguments = HtmlHelper.Unescape(argString);
|
||||
|
||||
return true;
|
||||
@@ -295,7 +312,9 @@ namespace Markdig.Parsers
|
||||
// Add the language as an attribute by default
|
||||
if (!string.IsNullOrEmpty(fenced.Info))
|
||||
{
|
||||
fenced.GetAttributes().AddClass(InfoPrefix + fenced.Info);
|
||||
Debug.Assert(_infoPrefixCache is not null || InfoPrefix is null);
|
||||
string infoWithPrefix = _infoPrefixCache?.Get(fenced.Info!) ?? fenced.Info!;
|
||||
fenced.GetAttributes().AddClass(infoWithPrefix);
|
||||
}
|
||||
|
||||
// Store the number of matched string into the context
|
||||
@@ -332,9 +351,13 @@ namespace Markdig.Parsers
|
||||
|
||||
var fencedBlock = (IFencedBlock)block;
|
||||
fencedBlock.ClosingFencedCharCount = closingCount;
|
||||
fencedBlock.NewLine = processor.Line.NewLine;
|
||||
fencedBlock.TriviaBeforeClosingFence = processor.UseTrivia(sourcePosition - 1);
|
||||
fencedBlock.TriviaAfter = new StringSlice(processor.Line.Text, lastFenceCharPosition, endBeforeTrim);
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
fencedBlock.NewLine = processor.Line.NewLine;
|
||||
fencedBlock.TriviaBeforeClosingFence = processor.UseTrivia(sourcePosition - 1);
|
||||
fencedBlock.TriviaAfter = new StringSlice(processor.Line.Text, lastFenceCharPosition, endBeforeTrim);
|
||||
}
|
||||
|
||||
// Don't keep the last line
|
||||
return BlockState.BreakDiscard;
|
||||
|
||||
@@ -26,13 +26,19 @@ namespace Markdig.Parsers
|
||||
|
||||
protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor)
|
||||
{
|
||||
return new FencedCodeBlock(this)
|
||||
var codeBlock = new FencedCodeBlock(this)
|
||||
{
|
||||
IndentCount = processor.Indent,
|
||||
LinesBefore = processor.UseLinesBefore(),
|
||||
TriviaBefore = processor.UseTrivia(processor.Start - 1),
|
||||
NewLine = processor.Line.NewLine,
|
||||
};
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
codeBlock.LinesBefore = processor.UseLinesBefore();
|
||||
codeBlock.TriviaBefore = processor.UseTrivia(processor.Start - 1);
|
||||
codeBlock.NewLine = processor.Line.NewLine;
|
||||
}
|
||||
|
||||
return codeBlock;
|
||||
}
|
||||
|
||||
public override BlockState TryContinue(BlockProcessor processor, Block block)
|
||||
|
||||
@@ -82,20 +82,25 @@ namespace Markdig.Parsers
|
||||
var headingBlock = new HeadingBlock(this)
|
||||
{
|
||||
HeaderChar = matchingChar,
|
||||
TriviaAfterAtxHeaderChar = trivia,
|
||||
Level = leadingCount,
|
||||
Column = column,
|
||||
Span = { Start = sourcePosition },
|
||||
TriviaBefore = processor.UseTrivia(sourcePosition - 1),
|
||||
LinesBefore = processor.UseLinesBefore(),
|
||||
NewLine = processor.Line.NewLine,
|
||||
};
|
||||
processor.NewBlocks.Push(headingBlock);
|
||||
if (!processor.TrackTrivia)
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
headingBlock.TriviaAfterAtxHeaderChar = trivia;
|
||||
headingBlock.TriviaBefore = processor.UseTrivia(sourcePosition - 1);
|
||||
headingBlock.LinesBefore = processor.UseLinesBefore();
|
||||
headingBlock.NewLine = processor.Line.NewLine;
|
||||
}
|
||||
else
|
||||
{
|
||||
processor.GoToColumn(column + leadingCount + 1);
|
||||
}
|
||||
|
||||
processor.NewBlocks.Push(headingBlock);
|
||||
|
||||
// Gives a chance to parse attributes
|
||||
TryParseAttributes?.Invoke(processor, ref processor.Line, headingBlock);
|
||||
|
||||
|
||||
@@ -62,10 +62,10 @@ namespace Markdig.Parsers
|
||||
|
||||
private BlockState TryParseTagType7(BlockProcessor state, StringSlice line, int startColumn, int startPosition)
|
||||
{
|
||||
var builder = StringBuilderCache.Local();
|
||||
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
var c = line.CurrentChar;
|
||||
var result = BlockState.None;
|
||||
if ((c == '/' && HtmlHelper.TryParseHtmlCloseTag(ref line, builder)) || HtmlHelper.TryParseHtmlTagOpenTag(ref line, builder))
|
||||
if ((c == '/' && HtmlHelper.TryParseHtmlCloseTag(ref line, ref builder)) || HtmlHelper.TryParseHtmlTagOpenTag(ref line, ref builder))
|
||||
{
|
||||
// Must be followed by whitespace only
|
||||
bool hasOnlySpaces = true;
|
||||
@@ -90,7 +90,7 @@ namespace Markdig.Parsers
|
||||
}
|
||||
}
|
||||
|
||||
builder.Length = 0;
|
||||
builder.Dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -270,20 +279,26 @@ namespace Markdig.Parsers
|
||||
|
||||
private BlockState CreateHtmlBlock(BlockProcessor state, HtmlBlockType type, int startColumn, int startPosition)
|
||||
{
|
||||
state.NewBlocks.Push(new HtmlBlock(this)
|
||||
var htmlBlock = new HtmlBlock(this)
|
||||
{
|
||||
Column = startColumn,
|
||||
Type = type,
|
||||
// By default, setup to the end of line
|
||||
Span = new SourceSpan(startPosition, startPosition + state.Line.End),
|
||||
//BeforeWhitespace = state.PopBeforeWhitespace(startPosition - 1),
|
||||
LinesBefore = state.UseLinesBefore(),
|
||||
NewLine = state.Line.NewLine,
|
||||
});
|
||||
};
|
||||
|
||||
if (state.TrackTrivia)
|
||||
{
|
||||
htmlBlock.LinesBefore = state.UseLinesBefore();
|
||||
htmlBlock.NewLine = state.Line.NewLine;
|
||||
}
|
||||
|
||||
state.NewBlocks.Push(htmlBlock);
|
||||
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 },
|
||||
@@ -341,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 }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace Markdig.Parsers
|
||||
{
|
||||
public override bool CanInterrupt(BlockProcessor processor, Block block)
|
||||
{
|
||||
return !(block is ParagraphBlock);
|
||||
return !block.IsParagraphBlock;
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
@@ -36,9 +36,14 @@ namespace Markdig.Parsers
|
||||
{
|
||||
Column = processor.Column,
|
||||
Span = new SourceSpan(processor.Start, processor.Line.End),
|
||||
LinesBefore = processor.UseLinesBefore(),
|
||||
NewLine = processor.Line.NewLine,
|
||||
};
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
codeBlock.LinesBefore = processor.UseLinesBefore();
|
||||
codeBlock.NewLine = processor.Line.NewLine;
|
||||
}
|
||||
|
||||
var codeBlockLine = new CodeBlockLine
|
||||
{
|
||||
TriviaBefore = processor.UseTrivia(sourceStartPosition - 1)
|
||||
@@ -68,8 +73,12 @@ namespace Markdig.Parsers
|
||||
if (line.Slice.IsEmpty)
|
||||
{
|
||||
codeBlock.Lines.RemoveAt(i);
|
||||
processor.LinesBefore ??= new List<StringSlice>();
|
||||
processor.LinesBefore.Add(line.Slice);
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
processor.LinesBefore ??= new List<StringSlice>();
|
||||
processor.LinesBefore.Add(line.Slice);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -92,12 +101,15 @@ namespace Markdig.Parsers
|
||||
|
||||
// lines
|
||||
var cb = (CodeBlock)block;
|
||||
var codeBlockLine = new CodeBlockLine
|
||||
{
|
||||
TriviaBefore = processor.UseTrivia(processor.Start - 1)
|
||||
};
|
||||
var codeBlockLine = new CodeBlockLine();
|
||||
|
||||
cb.CodeBlockLines.Add(codeBlockLine);
|
||||
cb.NewLine = processor.Line.NewLine; // ensure block newline is last newline
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
codeBlockLine.TriviaBefore = processor.UseTrivia(processor.Start - 1);
|
||||
cb.NewLine = processor.Line.NewLine; // ensure block newline is last newline
|
||||
}
|
||||
}
|
||||
|
||||
return BlockState.Continue;
|
||||
|
||||
@@ -6,6 +6,8 @@ using System;
|
||||
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;
|
||||
@@ -112,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)
|
||||
@@ -124,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>
|
||||
@@ -141,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
|
||||
@@ -163,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>
|
||||
@@ -321,9 +362,10 @@ namespace Markdig.Parsers
|
||||
var container = Block!.Inline!;
|
||||
for (int depth = 0; ; depth++)
|
||||
{
|
||||
if (container.LastChild is ContainerInline nextContainer && !nextContainer.IsClosed)
|
||||
Inline? lastChild = container.LastChild;
|
||||
if (lastChild is not null && lastChild.IsContainerInline && !lastChild.IsClosed)
|
||||
{
|
||||
container = nextContainer;
|
||||
container = Unsafe.As<ContainerInline>(lastChild);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using Markdig.Helpers;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Markdig.Parsers.Inlines
|
||||
{
|
||||
@@ -40,7 +41,7 @@ namespace Markdig.Parsers.Inlines
|
||||
|
||||
char c = slice.CurrentChar;
|
||||
|
||||
var builder = StringBuilderCache.Local();
|
||||
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
|
||||
// A backtick string is a string of one or more backtick characters (`) that is neither preceded nor followed by a backtick.
|
||||
// A code span begins with a backtick string and ends with a backtick string of equal length.
|
||||
@@ -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;
|
||||
@@ -98,33 +102,43 @@ namespace Markdig.Parsers.Inlines
|
||||
bool isMatching = false;
|
||||
if (closeSticks == openSticks)
|
||||
{
|
||||
string content;
|
||||
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 && builder.Length > 2 && builder[0] == ' ' && builder[builder.Length - 1] == ' ')
|
||||
if (!allSpace && contentSpan.Length > 2 && contentSpan[0] == ' ' && contentSpan[contentSpan.Length - 1] == ' ')
|
||||
{
|
||||
content = builder.ToString(1, builder.Length - 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
content = builder.ToString();
|
||||
content.Offset++;
|
||||
content.Length -= 2;
|
||||
}
|
||||
|
||||
int delimiterCount = Math.Min(openSticks, closeSticks);
|
||||
var spanStart = processor.GetSourcePosition(startPosition, out int line, out int column);
|
||||
var spanEnd = processor.GetSourcePosition(slice.Start - 1);
|
||||
processor.Inline = new CodeInline(content)
|
||||
var codeInline = new CodeInline(content)
|
||||
{
|
||||
Delimiter = match,
|
||||
ContentWithTrivia = new StringSlice(slice.Text, contentStart, contentEnd - 1),
|
||||
Span = new SourceSpan(spanStart, spanEnd),
|
||||
Line = line,
|
||||
Column = column,
|
||||
DelimiterCount = delimiterCount,
|
||||
};
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
codeInline.ContentWithTrivia = new StringSlice(slice.Text, contentStart, contentEnd - 1);
|
||||
}
|
||||
|
||||
processor.Inline = codeInline;
|
||||
isMatching = true;
|
||||
}
|
||||
|
||||
builder.Dispose();
|
||||
return isMatching;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
@@ -91,11 +92,13 @@ namespace Markdig.Parsers.Inlines
|
||||
|
||||
public bool PostProcess(InlineProcessor state, Inline? root, Inline? lastChild, int postInlineProcessorIndex, bool isFinalProcessing)
|
||||
{
|
||||
if (!(root is ContainerInline container))
|
||||
if (root is null || !root.IsContainerInline)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ContainerInline container = Unsafe.As<ContainerInline>(root);
|
||||
|
||||
List<EmphasisDelimiterInline>? delimiters = null;
|
||||
if (container is EmphasisDelimiterInline emphasisDelimiter)
|
||||
{
|
||||
@@ -113,16 +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);
|
||||
}
|
||||
|
||||
child = child.NextSibling;
|
||||
// Follow DelimiterInline (EmphasisDelimiter, TableDelimiter...)
|
||||
child = delimiterInline.FirstChild;
|
||||
}
|
||||
else
|
||||
{
|
||||
child = child.NextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
if (delimiters != null)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace Markdig.Parsers.Inlines
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
// Hard line breaks are for separating inline content within a block. Neither syntax for hard line breaks works at the end of a paragraph or other block element:
|
||||
if (!(processor.Block is ParagraphBlock))
|
||||
if (!processor.Block!.IsParagraphBlock)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Markdig.Parsers.Inlines
|
||||
{
|
||||
@@ -78,18 +79,23 @@ namespace Markdig.Parsers.Inlines
|
||||
|
||||
// Else we insert a LinkDelimiter
|
||||
slice.SkipChar();
|
||||
var labelWithTrivia = new StringSlice(slice.Text, labelWithTriviaSpan.Start, labelWithTriviaSpan.End);
|
||||
processor.Inline = new LinkDelimiterInline(this)
|
||||
var linkDelimiter = new LinkDelimiterInline(this)
|
||||
{
|
||||
Type = DelimiterType.Open,
|
||||
Label = label,
|
||||
LabelWithTrivia = labelWithTrivia,
|
||||
LabelSpan = processor.GetSourcePositionFromLocalSpan(labelSpan),
|
||||
IsImage = isImage,
|
||||
Span = new SourceSpan(startPosition, processor.GetSourcePosition(slice.Start - 1)),
|
||||
Line = line,
|
||||
Column = column
|
||||
};
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
linkDelimiter.LabelWithTrivia = new StringSlice(slice.Text, labelWithTriviaSpan.Start, labelWithTriviaSpan.End);
|
||||
}
|
||||
|
||||
processor.Inline = linkDelimiter;
|
||||
return true;
|
||||
|
||||
case ']':
|
||||
@@ -137,18 +143,13 @@ namespace Markdig.Parsers.Inlines
|
||||
// Create a default link if the callback was not found
|
||||
if (link is null)
|
||||
{
|
||||
var labelWithTrivia = new StringSlice(text.Text, labelWithriviaSpan.Start, labelWithriviaSpan.End);
|
||||
// Inline Link
|
||||
link = new LinkInline()
|
||||
var linkInline = new LinkInline()
|
||||
{
|
||||
Url = HtmlHelper.Unescape(linkRef.Url),
|
||||
Title = HtmlHelper.Unescape(linkRef.Title),
|
||||
Label = label,
|
||||
LabelSpan = labelSpan,
|
||||
LabelWithTrivia = labelWithTrivia,
|
||||
LinkRefDefLabel = linkRef.Label,
|
||||
LinkRefDefLabelWithTrivia = linkRef.LabelWithTrivia,
|
||||
LocalLabel = localLabel,
|
||||
UrlSpan = linkRef.UrlSpan,
|
||||
IsImage = parent.IsImage,
|
||||
IsShortcut = isShortcut,
|
||||
@@ -157,6 +158,16 @@ namespace Markdig.Parsers.Inlines
|
||||
Line = parent.Line,
|
||||
Column = parent.Column,
|
||||
};
|
||||
|
||||
if (state.TrackTrivia)
|
||||
{
|
||||
linkInline.LabelWithTrivia = new StringSlice(text.Text, labelWithriviaSpan.Start, labelWithriviaSpan.End);
|
||||
linkInline.LinkRefDefLabel = linkRef.Label;
|
||||
linkInline.LinkRefDefLabelWithTrivia = linkRef.LabelWithTrivia;
|
||||
linkInline.LocalLabel = localLabel;
|
||||
}
|
||||
|
||||
link = linkInline;
|
||||
}
|
||||
|
||||
if (link is ContainerInline containerLink)
|
||||
@@ -233,74 +244,18 @@ namespace Markdig.Parsers.Inlines
|
||||
|
||||
if (text.CurrentChar == '(')
|
||||
{
|
||||
LinkInline? link = null;
|
||||
|
||||
if (inlineState.TrackTrivia)
|
||||
{
|
||||
if (LinkHelper.TryParseInlineLinkTrivia(
|
||||
ref text,
|
||||
out string? url,
|
||||
out SourceSpan unescapedUrlSpan,
|
||||
out string? title,
|
||||
out SourceSpan unescapedTitleSpan,
|
||||
out char titleEnclosingCharacter,
|
||||
out SourceSpan linkSpan,
|
||||
out SourceSpan titleSpan,
|
||||
out SourceSpan triviaBeforeLink,
|
||||
out SourceSpan triviaAfterLink,
|
||||
out SourceSpan triviaAfterTitle,
|
||||
out bool urlHasPointyBrackets))
|
||||
{
|
||||
var wsBeforeLink = new StringSlice(text.Text, triviaBeforeLink.Start, triviaBeforeLink.End);
|
||||
var wsAfterLink = new StringSlice(text.Text, triviaAfterLink.Start, triviaAfterLink.End);
|
||||
var wsAfterTitle = new StringSlice(text.Text, triviaAfterTitle.Start, triviaAfterTitle.End);
|
||||
var unescapedUrl = new StringSlice(text.Text, unescapedUrlSpan.Start, unescapedUrlSpan.End);
|
||||
var unescapedTitle = new StringSlice(text.Text, unescapedTitleSpan.Start, unescapedTitleSpan.End);
|
||||
// Inline Link
|
||||
var link = new LinkInline()
|
||||
{
|
||||
TriviaBeforeUrl = wsBeforeLink,
|
||||
Url = HtmlHelper.Unescape(url),
|
||||
UnescapedUrl = unescapedUrl,
|
||||
UrlHasPointyBrackets = urlHasPointyBrackets,
|
||||
TriviaAfterUrl = wsAfterLink,
|
||||
Title = HtmlHelper.Unescape(title),
|
||||
UnescapedTitle = unescapedTitle,
|
||||
TitleEnclosingCharacter = titleEnclosingCharacter,
|
||||
TriviaAfterTitle = wsAfterTitle,
|
||||
IsImage = openParent.IsImage,
|
||||
LabelSpan = openParent.LabelSpan,
|
||||
UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan),
|
||||
TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan),
|
||||
Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start - 1)),
|
||||
Line = openParent.Line,
|
||||
Column = openParent.Column,
|
||||
};
|
||||
|
||||
openParent.ReplaceBy(link);
|
||||
// Notifies processor as we are creating an inline locally
|
||||
inlineState.Inline = link;
|
||||
|
||||
// Process emphasis delimiters
|
||||
inlineState.PostProcessInlines(0, link, null, false);
|
||||
|
||||
// If we have a link (and not an image),
|
||||
// we also set all [ delimiters before the opening delimiter to inactive.
|
||||
// (This will prevent us from getting links within links.)
|
||||
if (!openParent.IsImage)
|
||||
{
|
||||
MarkParentAsInactive(parentDelimiter);
|
||||
}
|
||||
|
||||
link.IsClosed = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
link = TryParseInlineLinkTrivia(ref text, inlineState, openParent);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (LinkHelper.TryParseInlineLink(ref text, out string? url, out string? title, out SourceSpan linkSpan, out SourceSpan titleSpan))
|
||||
{
|
||||
// Inline Link
|
||||
var link = new LinkInline()
|
||||
link = new LinkInline()
|
||||
{
|
||||
Url = HtmlHelper.Unescape(url),
|
||||
Title = HtmlHelper.Unescape(title),
|
||||
@@ -312,34 +267,36 @@ namespace Markdig.Parsers.Inlines
|
||||
Line = openParent.Line,
|
||||
Column = openParent.Column,
|
||||
};
|
||||
|
||||
openParent.ReplaceBy(link);
|
||||
// Notifies processor as we are creating an inline locally
|
||||
inlineState.Inline = link;
|
||||
|
||||
// Process emphasis delimiters
|
||||
inlineState.PostProcessInlines(0, link, null, false);
|
||||
|
||||
// If we have a link (and not an image),
|
||||
// we also set all [ delimiters before the opening delimiter to inactive.
|
||||
// (This will prevent us from getting links within links.)
|
||||
if (!openParent.IsImage)
|
||||
{
|
||||
MarkParentAsInactive(parentDelimiter);
|
||||
}
|
||||
|
||||
link.IsClosed = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (link is not null)
|
||||
{
|
||||
openParent.ReplaceBy(link);
|
||||
// Notifies processor as we are creating an inline locally
|
||||
inlineState.Inline = link;
|
||||
|
||||
// Process emphasis delimiters
|
||||
inlineState.PostProcessInlines(0, link, null, false);
|
||||
|
||||
// If we have a link (and not an image),
|
||||
// we also set all [ delimiters before the opening delimiter to inactive.
|
||||
// (This will prevent us from getting links within links.)
|
||||
if (!openParent.IsImage)
|
||||
{
|
||||
MarkParentAsInactive(parentDelimiter);
|
||||
}
|
||||
|
||||
link.IsClosed = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
text = savedText;
|
||||
}
|
||||
|
||||
var labelSpan = SourceSpan.Empty;
|
||||
string? label = null;
|
||||
SourceSpan labelWithTrivia = SourceSpan.Empty;
|
||||
bool isLabelSpanLocal = true;
|
||||
|
||||
bool isShortcut = false;
|
||||
@@ -363,9 +320,10 @@ namespace Markdig.Parsers.Inlines
|
||||
label = openParent.Label;
|
||||
isShortcut = true;
|
||||
}
|
||||
|
||||
if (label != null || LinkHelper.TryParseLabelTrivia(ref text, true, out label, out labelSpan))
|
||||
{
|
||||
labelWithTrivia = new SourceSpan(labelSpan.Start, labelSpan.End);
|
||||
SourceSpan labelWithTrivia = new SourceSpan(labelSpan.Start, labelSpan.End);
|
||||
if (isLabelSpanLocal)
|
||||
{
|
||||
labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan);
|
||||
@@ -399,9 +357,55 @@ namespace Markdig.Parsers.Inlines
|
||||
|
||||
inlineState.Inline = openParent.ReplaceBy(literal);
|
||||
return false;
|
||||
|
||||
static LinkInline? TryParseInlineLinkTrivia(ref StringSlice text, InlineProcessor inlineState, LinkDelimiterInline openParent)
|
||||
{
|
||||
if (LinkHelper.TryParseInlineLinkTrivia(
|
||||
ref text,
|
||||
out string? url,
|
||||
out SourceSpan unescapedUrlSpan,
|
||||
out string? title,
|
||||
out SourceSpan unescapedTitleSpan,
|
||||
out char titleEnclosingCharacter,
|
||||
out SourceSpan linkSpan,
|
||||
out SourceSpan titleSpan,
|
||||
out SourceSpan triviaBeforeLink,
|
||||
out SourceSpan triviaAfterLink,
|
||||
out SourceSpan triviaAfterTitle,
|
||||
out bool urlHasPointyBrackets))
|
||||
{
|
||||
var wsBeforeLink = new StringSlice(text.Text, triviaBeforeLink.Start, triviaBeforeLink.End);
|
||||
var wsAfterLink = new StringSlice(text.Text, triviaAfterLink.Start, triviaAfterLink.End);
|
||||
var wsAfterTitle = new StringSlice(text.Text, triviaAfterTitle.Start, triviaAfterTitle.End);
|
||||
var unescapedUrl = new StringSlice(text.Text, unescapedUrlSpan.Start, unescapedUrlSpan.End);
|
||||
var unescapedTitle = new StringSlice(text.Text, unescapedTitleSpan.Start, unescapedTitleSpan.End);
|
||||
|
||||
return new LinkInline()
|
||||
{
|
||||
TriviaBeforeUrl = wsBeforeLink,
|
||||
Url = HtmlHelper.Unescape(url),
|
||||
UnescapedUrl = unescapedUrl,
|
||||
UrlHasPointyBrackets = urlHasPointyBrackets,
|
||||
TriviaAfterUrl = wsAfterLink,
|
||||
Title = HtmlHelper.Unescape(title),
|
||||
UnescapedTitle = unescapedTitle,
|
||||
TitleEnclosingCharacter = titleEnclosingCharacter,
|
||||
TriviaAfterTitle = wsAfterTitle,
|
||||
IsImage = openParent.IsImage,
|
||||
LabelSpan = openParent.LabelSpan,
|
||||
UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan),
|
||||
TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan),
|
||||
Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start - 1)),
|
||||
Line = openParent.Line,
|
||||
Column = openParent.Column,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkParentAsInactive(Inline? inline)
|
||||
private static void MarkParentAsInactive(Inline? inline)
|
||||
{
|
||||
while (inline != null)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -86,15 +86,19 @@ 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())
|
||||
{
|
||||
// TODO: We remove the thematic break, as it will be created later, but this is inefficient, try to find another way
|
||||
var thematicBreak = processor.NewBlocks.Pop();
|
||||
var linesBefore = thematicBreak.LinesBefore;
|
||||
processor.LinesBefore = linesBefore;
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
processor.LinesBefore = thematicBreak.LinesBefore;
|
||||
}
|
||||
|
||||
return BlockState.None;
|
||||
}
|
||||
}
|
||||
@@ -259,10 +263,11 @@ namespace Markdig.Parsers
|
||||
// Starts/continue the list unless:
|
||||
// - an empty list item follows a paragraph
|
||||
// - an ordered list is not starting by '1'
|
||||
if ((block ?? state.LastBlock) is ParagraphBlock previousParagraph)
|
||||
block ??= state.LastBlock;
|
||||
if (block is not null && block.IsParagraphBlock)
|
||||
{
|
||||
if (state.IsBlankLine ||
|
||||
state.IsOpen(previousParagraph) && listInfo.BulletType == '1' && listInfo.OrderedStart is not "1")
|
||||
state.IsOpen(block) && listInfo.BulletType == '1' && listInfo.OrderedStart is not "1")
|
||||
{
|
||||
state.GoToColumn(initColumn);
|
||||
state.TriviaStart = savedTriviaStart; // restore changed TriviaStart state
|
||||
@@ -276,12 +281,17 @@ namespace Markdig.Parsers
|
||||
Column = initColumn,
|
||||
ColumnWidth = columnWidth,
|
||||
Order = order,
|
||||
SourceBullet = listInfo.SourceBullet,
|
||||
TriviaBefore = triviaBefore,
|
||||
Span = new SourceSpan(sourcePosition, sourceEndPosition),
|
||||
LinesBefore = state.UseLinesBefore(),
|
||||
NewLine = state.Line.NewLine,
|
||||
};
|
||||
|
||||
if (state.TrackTrivia)
|
||||
{
|
||||
newListItem.TriviaBefore = triviaBefore;
|
||||
newListItem.LinesBefore = state.UseLinesBefore();
|
||||
newListItem.NewLine = state.Line.NewLine;
|
||||
newListItem.SourceBullet = listInfo.SourceBullet;
|
||||
}
|
||||
|
||||
state.NewBlocks.Push(newListItem);
|
||||
|
||||
if (currentParent != null)
|
||||
@@ -313,8 +323,13 @@ namespace Markdig.Parsers
|
||||
OrderedDelimiter = listInfo.OrderedDelimiter,
|
||||
DefaultOrderedStart = listInfo.DefaultOrderedStart,
|
||||
OrderedStart = listInfo.OrderedStart,
|
||||
LinesBefore = state.UseLinesBefore(),
|
||||
};
|
||||
|
||||
if (state.TrackTrivia)
|
||||
{
|
||||
newList.LinesBefore = state.UseLinesBefore();
|
||||
}
|
||||
|
||||
state.NewBlocks.Push(newList);
|
||||
}
|
||||
return BlockState.Continue;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Syntax;
|
||||
|
||||
@@ -43,8 +44,8 @@ namespace Markdig.Parsers
|
||||
|
||||
if (pipeline.PreciseSourceLocation)
|
||||
{
|
||||
int roughLineCountEstimate = text.Length / 40;
|
||||
roughLineCountEstimate = Math.Min(4, Math.Max(512, roughLineCountEstimate));
|
||||
int roughLineCountEstimate = text.Length / 32;
|
||||
roughLineCountEstimate = Math.Max(4, Math.Min(512, roughLineCountEstimate));
|
||||
document.LineStartIndexes = new List<int>(roughLineCountEstimate);
|
||||
}
|
||||
|
||||
@@ -149,8 +150,9 @@ namespace Markdig.Parsers
|
||||
for (; item.Index < container.Count; item.Index++)
|
||||
{
|
||||
var block = container[item.Index];
|
||||
if (block is LeafBlock leafBlock)
|
||||
if (block.IsLeafBlock)
|
||||
{
|
||||
LeafBlock leafBlock = Unsafe.As<LeafBlock>(block);
|
||||
leafBlock.OnProcessInlinesBegin(inlineProcessor);
|
||||
if (leafBlock.ProcessInlines)
|
||||
{
|
||||
@@ -167,10 +169,10 @@ namespace Markdig.Parsers
|
||||
}
|
||||
leafBlock.OnProcessInlinesEnd(inlineProcessor);
|
||||
}
|
||||
else if (block is ContainerBlock newContainer)
|
||||
else if (block.IsContainerBlock)
|
||||
{
|
||||
// If we need to remove it
|
||||
if (newContainer.RemoveAfterProcessInlines)
|
||||
if (block.RemoveAfterProcessInlines)
|
||||
{
|
||||
container.RemoveAt(item.Index);
|
||||
}
|
||||
@@ -185,8 +187,8 @@ namespace Markdig.Parsers
|
||||
Array.Resize(ref blocks, blockCount * 2);
|
||||
ThrowHelper.CheckDepthLimit(blocks.Length);
|
||||
}
|
||||
blocks[blockCount++] = new ContainerItem(newContainer);
|
||||
newContainer.OnProcessInlinesBegin(inlineProcessor);
|
||||
blocks[blockCount++] = new ContainerItem(Unsafe.As<ContainerBlock>(block));
|
||||
block.OnProcessInlinesBegin(inlineProcessor);
|
||||
goto process_new_block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -23,13 +23,19 @@ namespace Markdig.Parsers
|
||||
}
|
||||
|
||||
// We continue trying to match by default
|
||||
processor.NewBlocks.Push(new ParagraphBlock(this)
|
||||
var paragraph = new ParagraphBlock(this)
|
||||
{
|
||||
Column = processor.Column,
|
||||
Span = new SourceSpan(processor.Line.Start, processor.Line.End),
|
||||
LinesBefore = processor.UseLinesBefore(),
|
||||
NewLine = processor.Line.NewLine,
|
||||
});
|
||||
};
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
paragraph.LinesBefore = processor.UseLinesBefore();
|
||||
paragraph.NewLine = processor.Line.NewLine;
|
||||
}
|
||||
|
||||
processor.NewBlocks.Push(paragraph);
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
@@ -128,15 +134,19 @@ namespace Markdig.Parsers
|
||||
Span = new SourceSpan(paragraph.Span.Start, line.Start),
|
||||
Level = level,
|
||||
Lines = paragraph.Lines,
|
||||
TriviaBefore = state.UseTrivia(sourcePosition - 1), // remove dashes
|
||||
TriviaAfter = new StringSlice(state.Line.Text, state.Start, line.End),
|
||||
LinesBefore = paragraph.LinesBefore,
|
||||
NewLine = state.Line.NewLine,
|
||||
IsSetext = true,
|
||||
HeaderCharCount = count,
|
||||
SetextNewline = paragraph.NewLine,
|
||||
};
|
||||
if (!state.TrackTrivia)
|
||||
|
||||
if (state.TrackTrivia)
|
||||
{
|
||||
heading.LinesBefore = paragraph.LinesBefore;
|
||||
heading.TriviaBefore = state.UseTrivia(sourcePosition - 1); // remove dashes
|
||||
heading.TriviaAfter = new StringSlice(state.Line.Text, state.Start, line.End);
|
||||
heading.NewLine = state.Line.NewLine;
|
||||
heading.SetextNewline = paragraph.NewLine;
|
||||
}
|
||||
else
|
||||
{
|
||||
heading.Lines.Trim();
|
||||
}
|
||||
|
||||
@@ -41,9 +41,13 @@ namespace Markdig.Parsers
|
||||
QuoteChar = quoteChar,
|
||||
Column = column,
|
||||
Span = new SourceSpan(sourcePosition, processor.Line.End),
|
||||
LinesBefore = processor.UseLinesBefore()
|
||||
};
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
quoteBlock.LinesBefore = processor.UseLinesBefore();
|
||||
}
|
||||
|
||||
bool hasSpaceAfterQuoteChar = false;
|
||||
if (c == ' ')
|
||||
{
|
||||
@@ -56,28 +60,34 @@ namespace Markdig.Parsers
|
||||
processor.NextColumn();
|
||||
}
|
||||
|
||||
var triviaBefore = processor.UseTrivia(sourcePosition - 1);
|
||||
StringSlice triviaAfter = StringSlice.Empty;
|
||||
bool wasEmptyLine = false;
|
||||
if (processor.Line.IsEmptyOrWhitespace())
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
processor.TriviaStart = processor.Start;
|
||||
triviaAfter = processor.UseTrivia(processor.Line.End);
|
||||
wasEmptyLine = true;
|
||||
var triviaBefore = processor.UseTrivia(sourcePosition - 1);
|
||||
StringSlice triviaAfter = StringSlice.Empty;
|
||||
bool wasEmptyLine = false;
|
||||
if (processor.Line.IsEmptyOrWhitespace())
|
||||
{
|
||||
processor.TriviaStart = processor.Start;
|
||||
triviaAfter = processor.UseTrivia(processor.Line.End);
|
||||
wasEmptyLine = true;
|
||||
}
|
||||
|
||||
if (!wasEmptyLine)
|
||||
{
|
||||
processor.TriviaStart = processor.Start;
|
||||
}
|
||||
|
||||
quoteBlock.QuoteLines.Add(new QuoteBlockLine
|
||||
{
|
||||
TriviaBefore = triviaBefore,
|
||||
TriviaAfter = triviaAfter,
|
||||
QuoteChar = true,
|
||||
HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar,
|
||||
NewLine = processor.Line.NewLine,
|
||||
});
|
||||
}
|
||||
quoteBlock.QuoteLines.Add(new QuoteBlockLine
|
||||
{
|
||||
TriviaBefore = triviaBefore,
|
||||
TriviaAfter = triviaAfter,
|
||||
QuoteChar = true,
|
||||
HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar,
|
||||
NewLine = processor.Line.NewLine,
|
||||
});
|
||||
|
||||
processor.NewBlocks.Push(quoteBlock);
|
||||
if (!wasEmptyLine)
|
||||
{
|
||||
processor.TriviaStart = processor.Start;
|
||||
}
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
@@ -94,7 +104,6 @@ namespace Markdig.Parsers
|
||||
// 5.1 Block quotes
|
||||
// A block quote marker consists of 0-3 spaces of initial indent, plus (a) the character > together with a following space, or (b) a single character > not followed by a space.
|
||||
var c = processor.CurrentChar;
|
||||
bool hasSpaceAfterQuoteChar = false;
|
||||
if (c != quote.QuoteChar)
|
||||
{
|
||||
if (processor.IsBlankLine)
|
||||
@@ -103,14 +112,19 @@ namespace Markdig.Parsers
|
||||
}
|
||||
else
|
||||
{
|
||||
quote.QuoteLines.Add(new QuoteBlockLine
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
QuoteChar = false,
|
||||
NewLine = processor.Line.NewLine,
|
||||
});
|
||||
quote.QuoteLines.Add(new QuoteBlockLine
|
||||
{
|
||||
QuoteChar = false,
|
||||
NewLine = processor.Line.NewLine,
|
||||
});
|
||||
}
|
||||
return BlockState.None;
|
||||
}
|
||||
}
|
||||
|
||||
bool hasSpaceAfterQuoteChar = false;
|
||||
c = processor.NextChar(); // Skip quote marker char
|
||||
if (c == ' ')
|
||||
{
|
||||
@@ -122,28 +136,33 @@ namespace Markdig.Parsers
|
||||
{
|
||||
processor.NextColumn();
|
||||
}
|
||||
var TriviaSpaceBefore = processor.UseTrivia(sourcePosition - 1);
|
||||
StringSlice triviaAfter = StringSlice.Empty;
|
||||
bool wasEmptyLine = false;
|
||||
if (processor.Line.IsEmptyOrWhitespace())
|
||||
{
|
||||
processor.TriviaStart = processor.Start;
|
||||
triviaAfter = processor.UseTrivia(processor.Line.End);
|
||||
wasEmptyLine = true;
|
||||
}
|
||||
quote.QuoteLines.Add(new QuoteBlockLine
|
||||
{
|
||||
QuoteChar = true,
|
||||
HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar,
|
||||
TriviaBefore = TriviaSpaceBefore,
|
||||
TriviaAfter = triviaAfter,
|
||||
NewLine = processor.Line.NewLine,
|
||||
});
|
||||
|
||||
if (!wasEmptyLine)
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
processor.TriviaStart = processor.Start;
|
||||
var triviaSpaceBefore = processor.UseTrivia(sourcePosition - 1);
|
||||
StringSlice triviaAfter = StringSlice.Empty;
|
||||
bool wasEmptyLine = false;
|
||||
if (processor.Line.IsEmptyOrWhitespace())
|
||||
{
|
||||
processor.TriviaStart = processor.Start;
|
||||
triviaAfter = processor.UseTrivia(processor.Line.End);
|
||||
wasEmptyLine = true;
|
||||
}
|
||||
quote.QuoteLines.Add(new QuoteBlockLine
|
||||
{
|
||||
QuoteChar = true,
|
||||
HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar,
|
||||
TriviaBefore = triviaSpaceBefore,
|
||||
TriviaAfter = triviaAfter,
|
||||
NewLine = processor.Line.NewLine,
|
||||
});
|
||||
|
||||
if (!wasEmptyLine)
|
||||
{
|
||||
processor.TriviaStart = processor.Start;
|
||||
}
|
||||
}
|
||||
|
||||
block.UpdateSpanEnd(processor.Line.End);
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ namespace Markdig.Parsers
|
||||
}
|
||||
|
||||
// Push a new block
|
||||
processor.NewBlocks.Push(new ThematicBreakBlock(this)
|
||||
var thematicBreak = new ThematicBreakBlock(this)
|
||||
{
|
||||
Column = processor.Column,
|
||||
Span = new SourceSpan(startPosition, line.End),
|
||||
@@ -94,10 +94,16 @@ namespace Markdig.Parsers
|
||||
// TODO: should we separate whitespace before/after?
|
||||
//BeforeWhitespace = beforeWhitespace,
|
||||
//AfterWhitespace = processor.PopBeforeWhitespace(processor.CurrentLineStartPosition),
|
||||
LinesBefore = processor.UseLinesBefore(),
|
||||
Content = new StringSlice(line.Text, processor.TriviaStart, line.End, line.NewLine), //include whitespace for now
|
||||
NewLine = processor.Line.NewLine,
|
||||
});
|
||||
};
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
thematicBreak.LinesBefore = processor.UseLinesBefore();
|
||||
thematicBreak.NewLine = processor.Line.NewLine;
|
||||
}
|
||||
|
||||
processor.NewBlocks.Push(thematicBreak);
|
||||
return BlockState.BreakDiscard;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ namespace System.Diagnostics.CodeAnalysis
|
||||
|
||||
public bool ReturnValue { get; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, Inherited = false)]
|
||||
public sealed class AllowNullAttribute : Attribute { }
|
||||
#endif
|
||||
|
||||
#if !NET5_0_OR_GREATER
|
||||
|
||||
17
src/Markdig/Polyfills/Unsafe.cs
Normal file
17
src/Markdig/Polyfills/Unsafe.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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 System.Runtime.CompilerServices
|
||||
{
|
||||
#if NETSTANDARD2_1
|
||||
internal static class Unsafe
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T As<T>(object o) where T : class
|
||||
{
|
||||
return (T)o;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -15,27 +15,25 @@ namespace Markdig.Renderers.Html
|
||||
/// <seealso cref="HtmlObjectRenderer{CodeBlock}" />
|
||||
public class CodeBlockRenderer : HtmlObjectRenderer<CodeBlock>
|
||||
{
|
||||
private HashSet<string>? _blocksAsDiv;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CodeBlockRenderer"/> class.
|
||||
/// </summary>
|
||||
public CodeBlockRenderer()
|
||||
{
|
||||
BlocksAsDiv = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
public CodeBlockRenderer() { }
|
||||
|
||||
public bool OutputAttributesOnPre { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a map of fenced code block infos that should be rendered as div blocks instead of pre/code blocks.
|
||||
/// </summary>
|
||||
public HashSet<string> BlocksAsDiv { get; }
|
||||
public HashSet<string> BlocksAsDiv => _blocksAsDiv ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
protected override void Write(HtmlRenderer renderer, CodeBlock obj)
|
||||
{
|
||||
renderer.EnsureLine();
|
||||
|
||||
var fencedCodeBlock = obj as FencedCodeBlock;
|
||||
if (fencedCodeBlock?.Info != null && BlocksAsDiv.Contains(fencedCodeBlock.Info))
|
||||
if (_blocksAsDiv is not null && (obj as FencedCodeBlock)?.Info is string info && _blocksAsDiv.Contains(info))
|
||||
{
|
||||
var infoPrefix = (obj.Parser as FencedCodeBlockParser)?.InfoPrefix ??
|
||||
FencedCodeBlockParser.DefaultInfoPrefix;
|
||||
@@ -48,7 +46,7 @@ namespace Markdig.Renderers.Html
|
||||
renderer.Write("<div")
|
||||
.WriteAttributes(obj.TryGetAttributes(),
|
||||
cls => cls.StartsWith(infoPrefix, StringComparison.Ordinal) ? cls.Substring(infoPrefix.Length) : cls)
|
||||
.Write('>');
|
||||
.WriteRaw('>');
|
||||
}
|
||||
|
||||
renderer.WriteLeafRawLines(obj, true, true, true);
|
||||
@@ -57,7 +55,6 @@ namespace Markdig.Renderers.Html
|
||||
{
|
||||
renderer.WriteLine("</div>");
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -70,14 +67,14 @@ namespace Markdig.Renderers.Html
|
||||
renderer.WriteAttributes(obj);
|
||||
}
|
||||
|
||||
renderer.Write("><code");
|
||||
renderer.WriteRaw("><code");
|
||||
|
||||
if (!OutputAttributesOnPre)
|
||||
{
|
||||
renderer.WriteAttributes(obj);
|
||||
}
|
||||
|
||||
renderer.Write('>');
|
||||
renderer.WriteRaw('>');
|
||||
}
|
||||
|
||||
renderer.WriteLeafRawLines(obj, true, true);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System.Globalization;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace Markdig.Renderers.Html
|
||||
@@ -25,20 +24,26 @@ namespace Markdig.Renderers.Html
|
||||
protected override void Write(HtmlRenderer renderer, HeadingBlock obj)
|
||||
{
|
||||
int index = obj.Level - 1;
|
||||
string headingText = ((uint)index < (uint)HeadingTexts.Length)
|
||||
? HeadingTexts[index]
|
||||
: "h" + obj.Level.ToString(CultureInfo.InvariantCulture);
|
||||
string[] headings = HeadingTexts;
|
||||
string headingText = ((uint)index < (uint)headings.Length)
|
||||
? headings[index]
|
||||
: $"h{obj.Level}";
|
||||
|
||||
if (renderer.EnableHtmlForBlock)
|
||||
{
|
||||
renderer.Write("<").Write(headingText).WriteAttributes(obj).Write('>');
|
||||
renderer.Write('<');
|
||||
renderer.WriteRaw(headingText);
|
||||
renderer.WriteAttributes(obj);
|
||||
renderer.WriteRaw('>');
|
||||
}
|
||||
|
||||
renderer.WriteLeafInline(obj);
|
||||
|
||||
if (renderer.EnableHtmlForBlock)
|
||||
{
|
||||
renderer.Write("</").Write(headingText).WriteLine(">");
|
||||
renderer.Write("</");
|
||||
renderer.WriteRaw(headingText);
|
||||
renderer.WriteLine('>');
|
||||
}
|
||||
|
||||
renderer.EnsureLine();
|
||||
|
||||
@@ -54,28 +54,26 @@ namespace Markdig.Renderers.Html.Inlines
|
||||
{
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write("<a href=\"");
|
||||
if (obj.IsEmail)
|
||||
{
|
||||
renderer.Write("mailto:");
|
||||
}
|
||||
renderer.Write(obj.IsEmail ? "<a href=\"mailto:" : "<a href=\"");
|
||||
renderer.WriteEscapeUrl(obj.Url);
|
||||
renderer.Write('"');
|
||||
renderer.WriteRaw('"');
|
||||
renderer.WriteAttributes(obj);
|
||||
|
||||
if (!obj.IsEmail && !string.IsNullOrWhiteSpace(Rel))
|
||||
{
|
||||
renderer.Write($" rel=\"{Rel}\"");
|
||||
renderer.WriteRaw(" rel=\"");
|
||||
renderer.WriteRaw(Rel);
|
||||
renderer.WriteRaw('"');
|
||||
}
|
||||
|
||||
renderer.Write('>');
|
||||
renderer.WriteRaw('>');
|
||||
}
|
||||
|
||||
renderer.WriteEscape(obj.Url);
|
||||
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write("</a>");
|
||||
renderer.WriteRaw("</a>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,19 +16,21 @@ namespace Markdig.Renderers.Html.Inlines
|
||||
{
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write("<code").WriteAttributes(obj).Write('>');
|
||||
renderer.Write("<code");
|
||||
renderer.WriteAttributes(obj);
|
||||
renderer.WriteRaw('>');
|
||||
}
|
||||
if (renderer.EnableHtmlEscape)
|
||||
{
|
||||
renderer.WriteEscape(obj.Content);
|
||||
renderer.WriteEscape(obj.ContentSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.Write(obj.Content);
|
||||
renderer.Write(obj.ContentSpan);
|
||||
}
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write("</code>");
|
||||
renderer.WriteRaw("</code>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,17 @@ namespace Markdig.Renderers.Html.Inlines
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
tag = GetTag(obj);
|
||||
renderer.Write("<").Write(tag).WriteAttributes(obj).Write('>');
|
||||
renderer.Write('<');
|
||||
renderer.WriteRaw(tag);
|
||||
renderer.WriteAttributes(obj);
|
||||
renderer.WriteRaw('>');
|
||||
}
|
||||
renderer.WriteChildren(obj);
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write("</").Write(tag).Write('>');
|
||||
renderer.Write("</");
|
||||
renderer.WriteRaw(tag);
|
||||
renderer.WriteRaw('>');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +58,7 @@ namespace Markdig.Renderers.Html.Inlines
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <returns></returns>
|
||||
public string? GetDefaultTag(EmphasisInline obj)
|
||||
public static string? GetDefaultTag(EmphasisInline obj)
|
||||
{
|
||||
if (obj.DelimiterChar is '*' or '_')
|
||||
{
|
||||
|
||||
@@ -51,14 +51,14 @@ namespace Markdig.Renderers.Html.Inlines
|
||||
{
|
||||
renderer.Write(link.IsImage ? "<img src=\"" : "<a href=\"");
|
||||
renderer.WriteEscapeUrl(link.GetDynamicUrl != null ? link.GetDynamicUrl() ?? link.Url : link.Url);
|
||||
renderer.Write('"');
|
||||
renderer.WriteRaw('"');
|
||||
renderer.WriteAttributes(link);
|
||||
}
|
||||
if (link.IsImage)
|
||||
{
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write(" alt=\"");
|
||||
renderer.WriteRaw(" alt=\"");
|
||||
}
|
||||
var wasEnableHtmlForInline = renderer.EnableHtmlForInline;
|
||||
renderer.EnableHtmlForInline = false;
|
||||
@@ -66,22 +66,22 @@ namespace Markdig.Renderers.Html.Inlines
|
||||
renderer.EnableHtmlForInline = wasEnableHtmlForInline;
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write('"');
|
||||
renderer.WriteRaw('"');
|
||||
}
|
||||
}
|
||||
|
||||
if (renderer.EnableHtmlForInline && !string.IsNullOrEmpty(link.Title))
|
||||
{
|
||||
renderer.Write(" title=\"");
|
||||
renderer.WriteRaw(" title=\"");
|
||||
renderer.WriteEscape(link.Title);
|
||||
renderer.Write('"');
|
||||
renderer.WriteRaw('"');
|
||||
}
|
||||
|
||||
if (link.IsImage)
|
||||
{
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write(" />");
|
||||
renderer.WriteRaw(" />");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -90,9 +90,11 @@ namespace Markdig.Renderers.Html.Inlines
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(Rel))
|
||||
{
|
||||
renderer.Write($" rel=\"{Rel}\"");
|
||||
renderer.WriteRaw(" rel=\"");
|
||||
renderer.WriteRaw(Rel);
|
||||
renderer.WriteRaw('"');
|
||||
}
|
||||
renderer.Write('>');
|
||||
renderer.WriteRaw('>');
|
||||
}
|
||||
renderer.WriteChildren(link);
|
||||
if (renderer.EnableHtmlForInline)
|
||||
|
||||
@@ -22,12 +22,16 @@ namespace Markdig.Renderers.Html
|
||||
renderer.Write("<ol");
|
||||
if (listBlock.BulletType != '1')
|
||||
{
|
||||
renderer.Write(" type=\"").Write(listBlock.BulletType).Write('"');
|
||||
renderer.WriteRaw(" type=\"");
|
||||
renderer.WriteRaw(listBlock.BulletType);
|
||||
renderer.WriteRaw('"');
|
||||
}
|
||||
|
||||
if (listBlock.OrderedStart != null && (listBlock.OrderedStart is not "1"))
|
||||
if (listBlock.OrderedStart is not null && listBlock.OrderedStart != "1")
|
||||
{
|
||||
renderer.Write(" start=\"").Write(listBlock.OrderedStart).Write('"');
|
||||
renderer.Write(" start=\"");
|
||||
renderer.WriteRaw(listBlock.OrderedStart);
|
||||
renderer.WriteRaw('"');
|
||||
}
|
||||
renderer.WriteAttributes(listBlock);
|
||||
renderer.WriteLine('>');
|
||||
@@ -49,7 +53,9 @@ namespace Markdig.Renderers.Html
|
||||
renderer.EnsureLine();
|
||||
if (renderer.EnableHtmlForBlock)
|
||||
{
|
||||
renderer.Write("<li").WriteAttributes(listItem).Write('>');
|
||||
renderer.Write("<li");
|
||||
renderer.WriteAttributes(listItem);
|
||||
renderer.WriteRaw('>');
|
||||
}
|
||||
|
||||
renderer.WriteChildren(listItem);
|
||||
|
||||
@@ -21,12 +21,14 @@ namespace Markdig.Renderers.Html
|
||||
renderer.EnsureLine();
|
||||
}
|
||||
|
||||
renderer.Write("<p").WriteAttributes(obj).Write(">");
|
||||
renderer.Write("<p");
|
||||
renderer.WriteAttributes(obj);
|
||||
renderer.WriteRaw('>');
|
||||
}
|
||||
renderer.WriteLeafInline(obj);
|
||||
if (!renderer.ImplicitParagraph)
|
||||
{
|
||||
if(renderer.EnableHtmlForBlock)
|
||||
if (renderer.EnableHtmlForBlock)
|
||||
{
|
||||
renderer.WriteLine("</p>");
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ namespace Markdig.Renderers.Html
|
||||
renderer.EnsureLine();
|
||||
if (renderer.EnableHtmlForBlock)
|
||||
{
|
||||
renderer.Write("<blockquote").WriteAttributes(obj).WriteLine(">");
|
||||
renderer.Write("<blockquote");
|
||||
renderer.WriteAttributes(obj);
|
||||
renderer.WriteLine('>');
|
||||
}
|
||||
var savedImplicitParagraph = renderer.ImplicitParagraph;
|
||||
renderer.ImplicitParagraph = false;
|
||||
|
||||
@@ -16,7 +16,9 @@ namespace Markdig.Renderers.Html
|
||||
{
|
||||
if (renderer.EnableHtmlForBlock)
|
||||
{
|
||||
renderer.Write("<hr").WriteAttributes(obj).WriteLine(" />");
|
||||
renderer.Write("<hr");
|
||||
renderer.WriteAttributes(obj);
|
||||
renderer.WriteLine(" />");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace Markdig.Renderers
|
||||
/// <seealso cref="TextRendererBase{HtmlRenderer}" />
|
||||
public class HtmlRenderer : TextRendererBase<HtmlRenderer>
|
||||
{
|
||||
private static readonly char[] s_writeEscapeIndexOfAnyChars = new[] { '<', '>', '&', '"' };
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HtmlRenderer"/> class.
|
||||
/// </summary>
|
||||
@@ -94,10 +96,7 @@ namespace Markdig.Renderers
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public HtmlRenderer WriteEscape(string? content)
|
||||
{
|
||||
if (content is { Length: > 0 })
|
||||
{
|
||||
WriteEscape(content, 0, content.Length);
|
||||
}
|
||||
WriteEscape(content.AsSpan());
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -110,11 +109,8 @@ namespace Markdig.Renderers
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public HtmlRenderer WriteEscape(ref StringSlice slice, bool softEscape = false)
|
||||
{
|
||||
if (slice.Start > slice.End)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
return WriteEscape(slice.Text, slice.Start, slice.Length, softEscape);
|
||||
WriteEscape(slice.AsSpan(), softEscape);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -126,7 +122,8 @@ namespace Markdig.Renderers
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public HtmlRenderer WriteEscape(StringSlice slice, bool softEscape = false)
|
||||
{
|
||||
return WriteEscape(ref slice, softEscape);
|
||||
WriteEscape(slice.AsSpan(), softEscape);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -139,58 +136,82 @@ namespace Markdig.Renderers
|
||||
/// <returns>This instance</returns>
|
||||
public HtmlRenderer WriteEscape(string content, int offset, int length, bool softEscape = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(content) || length == 0)
|
||||
return this;
|
||||
WriteEscape(content.AsSpan(offset, length), softEscape);
|
||||
return this;
|
||||
}
|
||||
|
||||
var end = offset + length;
|
||||
int previousOffset = offset;
|
||||
for (;offset < end; offset++)
|
||||
/// <summary>
|
||||
/// Writes the content escaped for HTML.
|
||||
/// </summary>
|
||||
/// <param name="content">The content.</param>
|
||||
/// <param name="softEscape">Only escape < and &</param>
|
||||
public void WriteEscape(ReadOnlySpan<char> content, bool softEscape = false)
|
||||
{
|
||||
if (!content.IsEmpty)
|
||||
{
|
||||
switch (content[offset])
|
||||
int nextIndex = content.IndexOfAny(s_writeEscapeIndexOfAnyChars);
|
||||
if (nextIndex == -1)
|
||||
{
|
||||
Write(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteEscapeSlow(content, softEscape);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteEscapeSlow(ReadOnlySpan<char> content, bool softEscape = false)
|
||||
{
|
||||
WriteIndent();
|
||||
|
||||
int previousOffset = 0;
|
||||
for (int i = 0; i < content.Length; i++)
|
||||
{
|
||||
switch (content[i])
|
||||
{
|
||||
case '<':
|
||||
Write(content, previousOffset, offset - previousOffset);
|
||||
WriteRaw(content.Slice(previousOffset, i - previousOffset));
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
Write("<");
|
||||
WriteRaw("<");
|
||||
}
|
||||
previousOffset = offset + 1;
|
||||
previousOffset = i + 1;
|
||||
break;
|
||||
case '>':
|
||||
if (!softEscape)
|
||||
{
|
||||
Write(content, previousOffset, offset - previousOffset);
|
||||
WriteRaw(content.Slice(previousOffset, i - previousOffset));
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
Write(">");
|
||||
WriteRaw(">");
|
||||
}
|
||||
previousOffset = offset + 1;
|
||||
previousOffset = i + 1;
|
||||
}
|
||||
break;
|
||||
case '&':
|
||||
Write(content, previousOffset, offset - previousOffset);
|
||||
WriteRaw(content.Slice(previousOffset, i - previousOffset));
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
Write("&");
|
||||
WriteRaw("&");
|
||||
}
|
||||
previousOffset = offset + 1;
|
||||
previousOffset = i + 1;
|
||||
break;
|
||||
case '"':
|
||||
if (!softEscape)
|
||||
{
|
||||
Write(content, previousOffset, offset - previousOffset);
|
||||
WriteRaw(content.Slice(previousOffset, i - previousOffset));
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
Write(""");
|
||||
WriteRaw(""");
|
||||
}
|
||||
previousOffset = offset + 1;
|
||||
previousOffset = i + 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Write(content, previousOffset, end - previousOffset);
|
||||
return this;
|
||||
WriteRaw(content.Slice(previousOffset));
|
||||
}
|
||||
|
||||
private static readonly IdnMapping IdnMapping = new IdnMapping();
|
||||
@@ -218,8 +239,8 @@ namespace Markdig.Renderers
|
||||
content = LinkRewriter(content);
|
||||
}
|
||||
|
||||
// ab://c.d = 8 chars
|
||||
int schemeOffset = content.Length < 8 ? -1 : content.IndexOf("://", 2, StringComparison.Ordinal);
|
||||
// a://c.d = 7 chars
|
||||
int schemeOffset = content.Length < 7 ? -1 : content.IndexOf("://", StringComparison.Ordinal);
|
||||
if (schemeOffset != -1) // This is an absolute URL
|
||||
{
|
||||
schemeOffset += 3; // skip ://
|
||||
@@ -360,7 +381,9 @@ namespace Markdig.Renderers
|
||||
|
||||
if (attributes.Id != null)
|
||||
{
|
||||
Write(" id=\"").WriteEscape(attributes.Id).Write('"');
|
||||
Write(" id=\"");
|
||||
WriteEscape(attributes.Id);
|
||||
WriteRaw('"');
|
||||
}
|
||||
|
||||
if (attributes.Classes is { Count: > 0 })
|
||||
@@ -371,21 +394,22 @@ namespace Markdig.Renderers
|
||||
var cssClass = attributes.Classes[i];
|
||||
if (i > 0)
|
||||
{
|
||||
Write(' ');
|
||||
WriteRaw(' ');
|
||||
}
|
||||
WriteEscape(classFilter != null ? classFilter(cssClass) : cssClass);
|
||||
}
|
||||
Write('"');
|
||||
WriteRaw('"');
|
||||
}
|
||||
|
||||
if (attributes.Properties is { Count: > 0 })
|
||||
{
|
||||
foreach (var property in attributes.Properties)
|
||||
{
|
||||
Write(' ').Write(property.Key);
|
||||
Write("=\"");
|
||||
Write(' ');
|
||||
WriteRaw(property.Key);
|
||||
WriteRaw("=\"");
|
||||
WriteEscape(property.Value ?? "");
|
||||
Write('"');
|
||||
WriteRaw('"');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,30 +427,40 @@ namespace Markdig.Renderers
|
||||
public HtmlRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfLines, bool escape, bool softEscape = false)
|
||||
{
|
||||
if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock();
|
||||
if (leafBlock.Lines.Lines != null)
|
||||
|
||||
var slices = leafBlock.Lines.Lines;
|
||||
if (slices is not null)
|
||||
{
|
||||
var lines = leafBlock.Lines;
|
||||
var slices = lines.Lines;
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
for (int i = 0; i < slices.Length; i++)
|
||||
{
|
||||
ref StringSlice slice = ref slices[i].Slice;
|
||||
if (slice.Text is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!writeEndOfLines && i > 0)
|
||||
{
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
ReadOnlySpan<char> span = slice.AsSpan();
|
||||
if (escape)
|
||||
{
|
||||
WriteEscape(ref slices[i].Slice, softEscape);
|
||||
WriteEscape(span, softEscape);
|
||||
}
|
||||
else
|
||||
{
|
||||
Write(ref slices[i].Slice);
|
||||
Write(span);
|
||||
}
|
||||
|
||||
if (writeEndOfLines)
|
||||
{
|
||||
WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using Markdig.Syntax;
|
||||
using System;
|
||||
|
||||
namespace Markdig.Renderers
|
||||
{
|
||||
@@ -15,9 +16,9 @@ namespace Markdig.Renderers
|
||||
/// Accepts the specified <see cref="MarkdownObject"/>.
|
||||
/// </summary>
|
||||
/// <param name="renderer">The renderer.</param>
|
||||
/// <param name="obj">The Markdown object.</param>
|
||||
/// <param name="objectType">The <see cref="Type"/> of the Markdown object.</param>
|
||||
/// <returns><c>true</c> If this renderer is accepting to render the specified Markdown object</returns>
|
||||
bool Accept(RendererBase renderer, MarkdownObject obj);
|
||||
bool Accept(RendererBase renderer, Type objectType);
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified <see cref="MarkdownObject"/> to the <paramref name="renderer"/>.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Syntax;
|
||||
using System;
|
||||
|
||||
namespace Markdig.Renderers
|
||||
{
|
||||
@@ -15,16 +16,15 @@ namespace Markdig.Renderers
|
||||
/// <seealso cref="IMarkdownObjectRenderer" />
|
||||
public abstract class MarkdownObjectRenderer<TRenderer, TObject> : IMarkdownObjectRenderer where TRenderer : RendererBase where TObject : MarkdownObject
|
||||
{
|
||||
protected MarkdownObjectRenderer()
|
||||
{
|
||||
TryWriters = new OrderedList<TryWriteDelegate>();
|
||||
}
|
||||
private OrderedList<TryWriteDelegate>? _tryWriters;
|
||||
|
||||
protected MarkdownObjectRenderer() { }
|
||||
|
||||
public delegate bool TryWriteDelegate(TRenderer renderer, TObject obj);
|
||||
|
||||
public virtual bool Accept(RendererBase renderer, MarkdownObject obj)
|
||||
public bool Accept(RendererBase renderer, Type objectType)
|
||||
{
|
||||
return obj is TObject;
|
||||
return typeof(TObject).IsAssignableFrom(objectType);
|
||||
}
|
||||
|
||||
public virtual void Write(RendererBase renderer, MarkdownObject obj)
|
||||
@@ -32,23 +32,31 @@ namespace Markdig.Renderers
|
||||
var htmlRenderer = (TRenderer)renderer;
|
||||
var typedObj = (TObject)obj;
|
||||
|
||||
// Try processing
|
||||
for (int i = 0; i < TryWriters.Count; i++)
|
||||
if (_tryWriters is not null && TryWrite(htmlRenderer, typedObj))
|
||||
{
|
||||
var tryWriter = TryWriters[i];
|
||||
if (tryWriter(htmlRenderer, typedObj))
|
||||
{
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Write(htmlRenderer, typedObj);
|
||||
}
|
||||
|
||||
private bool TryWrite(TRenderer renderer, TObject obj)
|
||||
{
|
||||
for (int i = 0; i < _tryWriters!.Count; i++)
|
||||
{
|
||||
var tryWriter = _tryWriters[i];
|
||||
if (tryWriter(renderer, obj))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional writers attached to this instance.
|
||||
/// </summary>
|
||||
public OrderedList<TryWriteDelegate> TryWriters { get; }
|
||||
public OrderedList<TryWriteDelegate> TryWriters => _tryWriters ??= new();
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified Markdown object to the renderer.
|
||||
|
||||
@@ -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(' ');
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ namespace Markdig.Renderers.Normalize.Inlines
|
||||
{
|
||||
protected override void Write(NormalizeRenderer renderer, LinkInline link)
|
||||
{
|
||||
if (link.IsAutoLink && !renderer.Options.ExpandAutoLinks)
|
||||
{
|
||||
renderer.Write(link.Url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (link.IsImage)
|
||||
{
|
||||
renderer.Write('!');
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
@@ -16,21 +19,32 @@ namespace Markdig.Renderers
|
||||
/// <seealso cref="IMarkdownRenderer" />
|
||||
public abstract class RendererBase : IMarkdownRenderer
|
||||
{
|
||||
private readonly Dictionary<Type, IMarkdownObjectRenderer> renderersPerType;
|
||||
private IMarkdownObjectRenderer? previousRenderer;
|
||||
private Type? previousObjectType;
|
||||
internal int childrenDepth = 0;
|
||||
private readonly Dictionary<Type, IMarkdownObjectRenderer?> _renderersPerType = new();
|
||||
internal int _childrenDepth = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RendererBase"/> class.
|
||||
/// </summary>
|
||||
protected RendererBase()
|
||||
protected RendererBase() { }
|
||||
|
||||
private IMarkdownObjectRenderer? GetRendererInstance(MarkdownObject obj)
|
||||
{
|
||||
ObjectRenderers = new ObjectRendererCollection();
|
||||
renderersPerType = new Dictionary<Type, IMarkdownObjectRenderer>();
|
||||
var key = obj.GetType();
|
||||
for (int i = 0; i < ObjectRenderers.Count; i++)
|
||||
{
|
||||
var renderer = ObjectRenderers[i];
|
||||
if (renderer.Accept(this, key))
|
||||
{
|
||||
_renderersPerType[key] = renderer;
|
||||
return renderer;
|
||||
}
|
||||
}
|
||||
|
||||
_renderersPerType[key] = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
public ObjectRendererCollection ObjectRenderers { get; }
|
||||
public ObjectRendererCollection ObjectRenderers { get; } = new();
|
||||
|
||||
public abstract object Render(MarkdownObject markdownObject);
|
||||
|
||||
@@ -59,7 +73,7 @@ namespace Markdig.Renderers
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.CheckDepthLimit(childrenDepth++);
|
||||
ThrowHelper.CheckDepthLimit(_childrenDepth++);
|
||||
|
||||
bool saveIsFirstInContainer = IsFirstInContainer;
|
||||
bool saveIsLastInContainer = IsLastInContainer;
|
||||
@@ -75,7 +89,7 @@ namespace Markdig.Renderers
|
||||
IsFirstInContainer = saveIsFirstInContainer;
|
||||
IsLastInContainer = saveIsLastInContainer;
|
||||
|
||||
childrenDepth--;
|
||||
_childrenDepth--;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -89,7 +103,7 @@ namespace Markdig.Renderers
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.CheckDepthLimit(childrenDepth++);
|
||||
ThrowHelper.CheckDepthLimit(_childrenDepth++);
|
||||
|
||||
bool saveIsFirstInContainer = IsFirstInContainer;
|
||||
bool saveIsLastInContainer = IsLastInContainer;
|
||||
@@ -110,7 +124,7 @@ namespace Markdig.Renderers
|
||||
IsFirstInContainer = saveIsFirstInContainer;
|
||||
IsLastInContainer = saveIsLastInContainer;
|
||||
|
||||
childrenDepth--;
|
||||
_childrenDepth--;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -127,42 +141,22 @@ namespace Markdig.Renderers
|
||||
// Calls before writing an object
|
||||
ObjectWriteBefore?.Invoke(this, obj);
|
||||
|
||||
var objectType = obj.GetType();
|
||||
|
||||
IMarkdownObjectRenderer? renderer;
|
||||
|
||||
// Handle regular renderers
|
||||
if (objectType == previousObjectType)
|
||||
if (!_renderersPerType.TryGetValue(obj.GetType(), out IMarkdownObjectRenderer? renderer))
|
||||
{
|
||||
renderer = previousRenderer;
|
||||
}
|
||||
else if (!renderersPerType.TryGetValue(objectType, out renderer))
|
||||
{
|
||||
for (int i = 0; i < ObjectRenderers.Count; i++)
|
||||
{
|
||||
var testRenderer = ObjectRenderers[i];
|
||||
if (testRenderer.Accept(this, obj))
|
||||
{
|
||||
renderersPerType[objectType] = renderer = testRenderer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
renderer = GetRendererInstance(obj);
|
||||
}
|
||||
|
||||
if (renderer != null)
|
||||
if (renderer is not null)
|
||||
{
|
||||
renderer.Write(this, obj);
|
||||
|
||||
previousObjectType = objectType;
|
||||
previousRenderer = renderer;
|
||||
}
|
||||
else if (obj is ContainerBlock containerBlock)
|
||||
else if (obj.IsContainerInline)
|
||||
{
|
||||
WriteChildren(containerBlock);
|
||||
WriteChildren(Unsafe.As<ContainerInline>(obj));
|
||||
}
|
||||
else if (obj is ContainerInline containerInline)
|
||||
else if (obj.IsContainerBlock)
|
||||
{
|
||||
WriteChildren(containerInline);
|
||||
WriteChildren(Unsafe.As<ContainerBlock>(obj));
|
||||
}
|
||||
|
||||
// Calls after writing an object
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Markdig.Helpers;
|
||||
@@ -18,7 +19,7 @@ namespace Markdig.Renderers
|
||||
/// <seealso cref="RendererBase" />
|
||||
public abstract class TextRendererBase : RendererBase
|
||||
{
|
||||
private TextWriter writer;
|
||||
private TextWriter _writer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextRendererBase"/> class.
|
||||
@@ -27,9 +28,7 @@ namespace Markdig.Renderers
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
protected TextRendererBase(TextWriter writer)
|
||||
{
|
||||
if (writer is null) ThrowHelper.ArgumentNullException_writer();
|
||||
this.writer = writer;
|
||||
this.writer.NewLine = "\n";
|
||||
Writer = writer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -38,7 +37,8 @@ namespace Markdig.Renderers
|
||||
/// <exception cref="ArgumentNullException">if the value is null</exception>
|
||||
public TextWriter Writer
|
||||
{
|
||||
get { return writer; }
|
||||
get => _writer;
|
||||
[MemberNotNull(nameof(_writer))]
|
||||
set
|
||||
{
|
||||
if (value is null)
|
||||
@@ -48,7 +48,7 @@ namespace Markdig.Renderers
|
||||
|
||||
// By default we output a newline with '\n' only even on Windows platforms
|
||||
value.NewLine = "\n";
|
||||
writer = value;
|
||||
_writer = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ namespace Markdig.Renderers
|
||||
|
||||
internal void ResetInternal()
|
||||
{
|
||||
childrenDepth = 0;
|
||||
_childrenDepth = 0;
|
||||
previousWasLine = true;
|
||||
indents.Clear();
|
||||
}
|
||||
@@ -146,11 +146,13 @@ namespace Markdig.Renderers
|
||||
/// Ensures a newline.
|
||||
/// </summary>
|
||||
/// <returns>This instance</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public T EnsureLine()
|
||||
{
|
||||
if (!previousWasLine)
|
||||
{
|
||||
WriteLine();
|
||||
previousWasLine = true;
|
||||
Writer.WriteLine();
|
||||
}
|
||||
return (T)this;
|
||||
}
|
||||
@@ -177,20 +179,25 @@ namespace Markdig.Renderers
|
||||
indents.RemoveAt(indents.Count - 1);
|
||||
}
|
||||
|
||||
private void WriteIndent()
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private protected void WriteIndent()
|
||||
{
|
||||
if (previousWasLine)
|
||||
{
|
||||
previousWasLine = false;
|
||||
for (int i = 0; i < indents.Count; i++)
|
||||
{
|
||||
var indent = indents[i];
|
||||
var indentText = indent.Next();
|
||||
Writer.Write(indentText);
|
||||
}
|
||||
WriteIndentCore();
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteIndentCore()
|
||||
{
|
||||
previousWasLine = false;
|
||||
for (int i = 0; i < indents.Count; i++)
|
||||
{
|
||||
var indent = indents[i];
|
||||
var indentText = indent.Next();
|
||||
Writer.Write(indentText);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified content.
|
||||
@@ -201,9 +208,8 @@ namespace Markdig.Renderers
|
||||
public T Write(string? content)
|
||||
{
|
||||
WriteIndent();
|
||||
previousWasLine = false;
|
||||
Writer.Write(content);
|
||||
return (T) this;
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -214,11 +220,8 @@ namespace Markdig.Renderers
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public T Write(ref StringSlice slice)
|
||||
{
|
||||
if (slice.Start > slice.End)
|
||||
{
|
||||
return (T) this;
|
||||
}
|
||||
return Write(slice.Text, slice.Start, slice.Length);
|
||||
Write(slice.AsSpan());
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -229,7 +232,8 @@ namespace Markdig.Renderers
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public T Write(StringSlice slice)
|
||||
{
|
||||
return Write(ref slice);
|
||||
Write(slice.AsSpan());
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -241,9 +245,12 @@ namespace Markdig.Renderers
|
||||
public T Write(char content)
|
||||
{
|
||||
WriteIndent();
|
||||
previousWasLine = content == '\n';
|
||||
if (content == '\n')
|
||||
{
|
||||
previousWasLine = true;
|
||||
}
|
||||
Writer.Write(content);
|
||||
return (T) this;
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -255,36 +262,49 @@ namespace Markdig.Renderers
|
||||
/// <returns>This instance</returns>
|
||||
public T Write(string content, int offset, int length)
|
||||
{
|
||||
if (content is null)
|
||||
if (content is not null)
|
||||
{
|
||||
return (T) this;
|
||||
Write(content.AsSpan(offset, length));
|
||||
}
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
WriteIndent();
|
||||
previousWasLine = false;
|
||||
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||
Writer.Write(content.AsSpan(offset, length));
|
||||
#else
|
||||
if (offset == 0 && content.Length == length)
|
||||
/// <summary>
|
||||
/// Writes the specified content.
|
||||
/// </summary>
|
||||
/// <param name="content">The content.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(ReadOnlySpan<char> content)
|
||||
{
|
||||
if (!content.IsEmpty)
|
||||
{
|
||||
Writer.Write(content);
|
||||
WriteIndent();
|
||||
WriteRaw(content);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void WriteRaw(char content) => Writer.Write(content);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void WriteRaw(string? content) => Writer.Write(content);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void WriteRaw(ReadOnlySpan<char> content)
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||
Writer.Write(content);
|
||||
#else
|
||||
if (content.Length > buffer.Length)
|
||||
{
|
||||
buffer = content.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (length > buffer.Length)
|
||||
{
|
||||
buffer = content.ToCharArray();
|
||||
Writer.Write(buffer, offset, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
content.CopyTo(offset, buffer, 0, length);
|
||||
Writer.Write(buffer, 0, length);
|
||||
}
|
||||
content.CopyTo(buffer);
|
||||
}
|
||||
Writer.Write(buffer, 0, content.Length);
|
||||
#endif
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -297,7 +317,7 @@ namespace Markdig.Renderers
|
||||
WriteIndent();
|
||||
Writer.WriteLine();
|
||||
previousWasLine = true;
|
||||
return (T) this;
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -324,7 +344,7 @@ namespace Markdig.Renderers
|
||||
WriteIndent();
|
||||
previousWasLine = true;
|
||||
Writer.WriteLine(content);
|
||||
return (T) this;
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -350,15 +370,15 @@ namespace Markdig.Renderers
|
||||
public T WriteLeafInline(LeafBlock leafBlock)
|
||||
{
|
||||
if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock();
|
||||
var inline = (Inline) leafBlock.Inline!;
|
||||
|
||||
Inline? inline = leafBlock.Inline;
|
||||
|
||||
while (inline != null)
|
||||
{
|
||||
Write(inline);
|
||||
inline = inline.NextSibling;
|
||||
}
|
||||
|
||||
return (T) this;
|
||||
|
||||
return (T)this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,14 +111,14 @@ All trivia in a document should be attached to a node. The `Block` class defines
|
||||
/// <summary>
|
||||
/// Gets or sets the trivia right before this block.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
public StringSlice TriviaBefore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets trivia occurring after this block.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
public StringSlice TriviaAfter { get; set; }
|
||||
```
|
||||
|
||||
7
src/Markdig/SkipLocalsInit.cs
Normal file
7
src/Markdig/SkipLocalsInit.cs
Normal file
@@ -0,0 +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.
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
[module: System.Runtime.CompilerServices.SkipLocalsInit]
|
||||
#endif
|
||||
@@ -14,6 +14,9 @@ namespace Markdig.Syntax
|
||||
/// <seealso cref="MarkdownObject" />
|
||||
public abstract class Block : MarkdownObject, IBlock
|
||||
{
|
||||
private BlockTriviaProperties? _trivia => GetTrivia<BlockTriviaProperties>();
|
||||
private BlockTriviaProperties Trivia => GetOrSetTrivia<BlockTriviaProperties>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Block"/> class.
|
||||
/// </summary>
|
||||
@@ -23,18 +26,23 @@ namespace Markdig.Syntax
|
||||
Parser = parser;
|
||||
IsOpen = true;
|
||||
IsBreakable = true;
|
||||
SetTypeKind(isInline: false, isContainer: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent of this container. May be null.
|
||||
/// </summary>
|
||||
public ContainerBlock? Parent { get; internal set; }
|
||||
public ContainerBlock? Parent { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parser associated to this instance.
|
||||
/// </summary>
|
||||
public BlockParser? Parser { get; }
|
||||
|
||||
internal bool IsLeafBlock { get; private protected set; }
|
||||
|
||||
internal bool IsParagraphBlock { get; private protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is still open.
|
||||
/// </summary>
|
||||
@@ -46,7 +54,8 @@ namespace Markdig.Syntax
|
||||
public bool IsBreakable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The last newline of this block
|
||||
/// The last newline of this block.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled
|
||||
/// </summary>
|
||||
public NewLine NewLine { get; set; }
|
||||
|
||||
@@ -58,28 +67,28 @@ namespace Markdig.Syntax
|
||||
/// <summary>
|
||||
/// Gets or sets the trivia right before this block.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
public StringSlice TriviaBefore { get; set; }
|
||||
public StringSlice TriviaBefore { get => _trivia?.TriviaBefore ?? StringSlice.Empty; set => Trivia.TriviaBefore = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets trivia occurring after this block.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
public StringSlice TriviaAfter { get; set; }
|
||||
public StringSlice TriviaAfter { get => _trivia?.TriviaAfter ?? StringSlice.Empty; set => Trivia.TriviaAfter = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the empty lines occurring before this block.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise null.
|
||||
/// </summary>
|
||||
public List<StringSlice>? LinesBefore { get; set; }
|
||||
public List<StringSlice>? LinesBefore { get => _trivia?.LinesBefore; set => Trivia.LinesBefore = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the empty lines occurring after this block.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise null.
|
||||
/// </summary>
|
||||
public List<StringSlice>? LinesAfter { get; set; }
|
||||
public List<StringSlice>? LinesAfter { get => _trivia?.LinesAfter; set => Trivia.LinesAfter = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the process of inlines begin.
|
||||
@@ -128,11 +137,30 @@ namespace Markdig.Syntax
|
||||
|
||||
internal static Block FindRootMostContainerParent(Block block)
|
||||
{
|
||||
while (block.Parent is ContainerBlock && block.Parent is not MarkdownDocument)
|
||||
while (true)
|
||||
{
|
||||
block = block.Parent;
|
||||
Block? parent = block.Parent;
|
||||
if (parent is null || !parent.IsContainerBlock || parent is MarkdownDocument)
|
||||
{
|
||||
break;
|
||||
}
|
||||
block = parent;
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
private protected T? TryGetDerivedTrivia<T>() where T : class => _trivia?.DerivedTriviaSlot as T;
|
||||
private protected T GetOrSetDerivedTrivia<T>() where T : new() => (T)(Trivia.DerivedTriviaSlot ??= new T());
|
||||
|
||||
private sealed class BlockTriviaProperties
|
||||
{
|
||||
// Used by derived types to store their own TriviaProperties
|
||||
public object? DerivedTriviaSlot;
|
||||
|
||||
public StringSlice TriviaBefore;
|
||||
public StringSlice TriviaAfter;
|
||||
public List<StringSlice>? LinesBefore;
|
||||
public List<StringSlice>? LinesAfter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,8 @@ namespace Markdig.Syntax
|
||||
public StringSlice TriviaBefore { get; set; }
|
||||
}
|
||||
|
||||
public List<CodeBlockLine> CodeBlockLines { get; } = new ();
|
||||
private List<CodeBlockLine>? _codeBlockLines;
|
||||
public List<CodeBlockLine> CodeBlockLines => _codeBlockLines ??= new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CodeBlock"/> class.
|
||||
|
||||
@@ -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,13 +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>();
|
||||
_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.
|
||||
@@ -64,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;
|
||||
}
|
||||
@@ -149,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;
|
||||
}
|
||||
@@ -172,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
|
||||
{
|
||||
@@ -223,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
|
||||
@@ -295,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ namespace Markdig.Syntax
|
||||
/// </remarks>
|
||||
public class FencedCodeBlock : CodeBlock, IFencedBlock
|
||||
{
|
||||
private TriviaProperties? _trivia => TryGetDerivedTrivia<TriviaProperties>();
|
||||
private TriviaProperties Trivia => GetOrSetDerivedTrivia<TriviaProperties>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FencedCodeBlock"/> class.
|
||||
/// </summary>
|
||||
@@ -41,33 +44,44 @@ namespace Markdig.Syntax
|
||||
public int OpeningFencedCharCount { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringSlice TriviaAfterFencedChar { get; set; }
|
||||
public StringSlice TriviaAfterFencedChar { get => _trivia?.TriviaAfterFencedChar ?? StringSlice.Empty; set => Trivia.TriviaAfterFencedChar = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? Info { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringSlice UnescapedInfo { get; set; }
|
||||
public StringSlice UnescapedInfo { get => _trivia?.UnescapedInfo ?? StringSlice.Empty; set => Trivia.UnescapedInfo = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringSlice TriviaAfterInfo { get; set; }
|
||||
public StringSlice TriviaAfterInfo { get => _trivia?.TriviaAfterInfo ?? StringSlice.Empty; set => Trivia.TriviaAfterInfo = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? Arguments { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringSlice UnescapedArguments { get; set; }
|
||||
public StringSlice UnescapedArguments { get => _trivia?.UnescapedArguments ?? StringSlice.Empty; set => Trivia.UnescapedArguments = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringSlice TriviaAfterArguments { get; set; }
|
||||
public StringSlice TriviaAfterArguments { get => _trivia?.TriviaAfterArguments ?? StringSlice.Empty; set => Trivia.TriviaAfterArguments = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public NewLine InfoNewLine { get; set; }
|
||||
public NewLine InfoNewLine { get => _trivia?.InfoNewLine ?? NewLine.None; set => Trivia.InfoNewLine = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringSlice TriviaBeforeClosingFence { get; set; }
|
||||
public StringSlice TriviaBeforeClosingFence { get => _trivia?.TriviaBeforeClosingFence ?? StringSlice.Empty; set => Trivia.TriviaBeforeClosingFence = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int ClosingFencedCharCount { get; set; }
|
||||
|
||||
private sealed class TriviaProperties
|
||||
{
|
||||
public StringSlice TriviaAfterFencedChar;
|
||||
public StringSlice UnescapedInfo;
|
||||
public StringSlice TriviaAfterInfo;
|
||||
public StringSlice UnescapedArguments;
|
||||
public StringSlice TriviaAfterArguments;
|
||||
public NewLine InfoNewLine;
|
||||
public StringSlice TriviaBeforeClosingFence;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,9 @@ namespace Markdig.Syntax
|
||||
[DebuggerDisplay("{GetType().Name} Line: {Line}, {Lines} Level: {Level}")]
|
||||
public class HeadingBlock : LeafBlock
|
||||
{
|
||||
private TriviaProperties? _trivia => TryGetDerivedTrivia<TriviaProperties>();
|
||||
private TriviaProperties Trivia => GetOrSetDerivedTrivia<TriviaProperties>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HeadingBlock"/> class.
|
||||
/// </summary>
|
||||
@@ -45,14 +48,21 @@ namespace Markdig.Syntax
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the newline of the first line when <see cref="IsSetext"/> is true.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled.
|
||||
/// </summary>
|
||||
public NewLine SetextNewline { get; set; }
|
||||
public NewLine SetextNewline { get => _trivia?.SetextNewline ?? NewLine.None; set => Trivia.SetextNewline = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the whitespace after the # character when <see cref="IsSetext"/> is false.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
public StringSlice TriviaAfterAtxHeaderChar { get; set; }
|
||||
public StringSlice TriviaAfterAtxHeaderChar { get => _trivia?.TriviaAfterAtxHeaderChar ?? StringSlice.Empty; set => Trivia.TriviaAfterAtxHeaderChar = value; }
|
||||
|
||||
private sealed class TriviaProperties
|
||||
{
|
||||
public NewLine SetextNewline;
|
||||
public StringSlice TriviaAfterAtxHeaderChar;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,13 +61,13 @@ namespace Markdig.Syntax
|
||||
/// <summary>
|
||||
/// Trivia occurring before this block
|
||||
/// </summary>
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise <see cref="StringSlice.IsEmpty"/>.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise <see cref="StringSlice.Empty"/>.
|
||||
StringSlice TriviaBefore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Trivia occurring after this block
|
||||
/// </summary>
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise <see cref="StringSlice.IsEmpty"/>.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise <see cref="StringSlice.Empty"/>.
|
||||
StringSlice TriviaAfter { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
|
||||
namespace Markdig.Syntax
|
||||
{
|
||||
@@ -25,7 +24,7 @@ namespace Markdig.Syntax
|
||||
/// <summary>
|
||||
/// Gets or sets the trivia after the <see cref="FencedChar"/>.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
StringSlice TriviaAfterFencedChar { get; set; }
|
||||
|
||||
@@ -38,14 +37,14 @@ namespace Markdig.Syntax
|
||||
/// <summary>
|
||||
/// Non-escaped <see cref="Info"/> exactly as in source markdown.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
StringSlice UnescapedInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the trivia after the <see cref="Info"/>.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
StringSlice TriviaAfterInfo { get; set; }
|
||||
|
||||
@@ -58,28 +57,28 @@ namespace Markdig.Syntax
|
||||
/// <summary>
|
||||
/// Non-escaped <see cref="Arguments"/> exactly as in source markdown.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
StringSlice UnescapedArguments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the trivia after the <see cref="Arguments"/>.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
StringSlice TriviaAfterArguments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Newline of the line with the opening fenced chars.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="NewLine.None"/>.
|
||||
/// </summary>
|
||||
NewLine InfoNewLine { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Trivia before the closing fenced chars
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
StringSlice TriviaBeforeClosingFence { get; set; }
|
||||
|
||||
@@ -92,7 +91,7 @@ namespace Markdig.Syntax
|
||||
/// Newline after the last line, which is always the line containing the closing fence chars.
|
||||
/// "Inherited" from <see cref="Block.NewLine"/>.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="NewLine.None"/>.
|
||||
/// </summary>
|
||||
NewLine NewLine { get; set; }
|
||||
}
|
||||
|
||||
@@ -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,9 +15,16 @@ namespace Markdig.Syntax.Inlines
|
||||
[DebuggerDisplay("`{Content}`")]
|
||||
public class CodeInline : LeafInline
|
||||
{
|
||||
public CodeInline(string content)
|
||||
private TriviaProperties? _trivia => GetTrivia<TriviaProperties>();
|
||||
private TriviaProperties Trivia => GetOrSetTrivia<TriviaProperties>();
|
||||
|
||||
private LazySubstring _content;
|
||||
|
||||
public CodeInline(string content) : this(new LazySubstring(content)) { }
|
||||
|
||||
internal CodeInline(LazySubstring content)
|
||||
{
|
||||
Content = content;
|
||||
_content = content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -32,21 +40,24 @@ 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.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
public StringSlice ContentWithTrivia { get; set; }
|
||||
public StringSlice ContentWithTrivia { get => _trivia?.ContentWithTrivia ?? StringSlice.Empty; set => Trivia.ContentWithTrivia = value; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the first and last character of the content enclosed in a backtick `
|
||||
/// is a space.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// false.
|
||||
/// </summary>
|
||||
public bool FirstAndLastWasSpace { get; set; }
|
||||
private sealed class TriviaProperties
|
||||
{
|
||||
public StringSlice ContentWithTrivia;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,11 @@ namespace Markdig.Syntax.Inlines
|
||||
/// <seealso cref="Inline" />
|
||||
public class ContainerInline : Inline, IEnumerable<Inline>
|
||||
{
|
||||
public ContainerInline()
|
||||
{
|
||||
SetTypeKind(isInline: true, isContainer: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent block of this inline.
|
||||
/// </summary>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
@@ -33,7 +39,11 @@ namespace Markdig.Syntax.Inlines
|
||||
/// <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.
|
||||
@@ -151,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;
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace Markdig.Syntax.Inlines
|
||||
/// <seealso cref="DelimiterInline" />
|
||||
public class LinkDelimiterInline : DelimiterInline
|
||||
{
|
||||
private TriviaProperties? _trivia => GetTrivia<TriviaProperties>();
|
||||
private TriviaProperties Trivia => GetOrSetTrivia<TriviaProperties>();
|
||||
|
||||
public LinkDelimiterInline(InlineParser parser) : base(parser)
|
||||
{
|
||||
}
|
||||
@@ -35,13 +38,18 @@ namespace Markdig.Syntax.Inlines
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Label"/> with trivia.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
public StringSlice LabelWithTrivia { get; set; }
|
||||
public StringSlice LabelWithTrivia { get => _trivia?.LabelWithTrivia ?? StringSlice.Empty; set => Trivia.LabelWithTrivia = value; }
|
||||
|
||||
public override string ToLiteral()
|
||||
{
|
||||
return IsImage ? "![" : "[";
|
||||
}
|
||||
|
||||
private sealed class TriviaProperties
|
||||
{
|
||||
public StringSlice LabelWithTrivia;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,13 @@ using System.Diagnostics;
|
||||
|
||||
namespace Markdig.Syntax.Inlines
|
||||
{
|
||||
public enum LocalLabel
|
||||
public enum LocalLabel : byte
|
||||
{
|
||||
Local, // [foo][bar]
|
||||
Empty, // [foo][]
|
||||
None, // [foo]
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A Link inline (Section 6.5 CommonMark specs)
|
||||
/// </summary>
|
||||
@@ -20,6 +21,9 @@ namespace Markdig.Syntax.Inlines
|
||||
[DebuggerDisplay("Url: {Url} Title: {Title} Image: {IsImage}")]
|
||||
public class LinkInline : ContainerInline
|
||||
{
|
||||
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
|
||||
/// of a Url.
|
||||
@@ -58,21 +62,21 @@ 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.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
public StringSlice LabelWithTrivia { get; set; }
|
||||
public StringSlice LabelWithTrivia { get => _trivia?.LabelWithTrivia ?? StringSlice.Empty; set => Trivia.LabelWithTrivia = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of label parsed
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="LocalLabel.None"/>.
|
||||
/// </summary>
|
||||
public LocalLabel LocalLabel { get; set; }
|
||||
public LocalLabel LocalLabel { get => _trivia?.LocalLabel ?? LocalLabel.None; set => Trivia.LocalLabel = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reference this link is attached to. May be null.
|
||||
@@ -81,21 +85,22 @@ namespace Markdig.Syntax.Inlines
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the label as matched against the <see cref="LinkReferenceDefinition"/>.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled.
|
||||
/// </summary>
|
||||
public string? LinkRefDefLabel { get; set; }
|
||||
public string? LinkRefDefLabel { get => _trivia?.LinkRefDefLabel; set => Trivia.LinkRefDefLabel = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="LinkRefDefLabel"/> with trivia as matched against
|
||||
/// the <see cref="LinkReferenceDefinition"/>
|
||||
/// </summary>
|
||||
public StringSlice LinkRefDefLabelWithTrivia { get; set; }
|
||||
public StringSlice LinkRefDefLabelWithTrivia { get => _trivia?.LinkRefDefLabelWithTrivia ?? StringSlice.Empty; set => Trivia.LinkRefDefLabelWithTrivia = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the trivia before the <see cref="Url"/>.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
public StringSlice TriviaBeforeUrl { get; set; }
|
||||
public StringSlice TriviaBeforeUrl { get => _trivia?.TriviaBeforeUrl ?? StringSlice.Empty; set => Trivia.TriviaBeforeUrl = value; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the <see cref="Url"/> in the source document is enclosed
|
||||
@@ -103,7 +108,7 @@ namespace Markdig.Syntax.Inlines
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// false.
|
||||
/// </summary>
|
||||
public bool UrlHasPointyBrackets { get; set; }
|
||||
public bool UrlHasPointyBrackets { get => _trivia?.UrlHasPointyBrackets ?? false; set => Trivia.UrlHasPointyBrackets = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL.
|
||||
@@ -113,21 +118,21 @@ 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
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
public StringSlice UnescapedUrl { get; set; }
|
||||
public StringSlice UnescapedUrl { get => _trivia?.UnescapedUrl ?? StringSlice.Empty; set => Trivia.UnescapedUrl = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Any trivia after the <see cref="Url"/>.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
public StringSlice TriviaAfterUrl { get; set; }
|
||||
public StringSlice TriviaAfterUrl { get => _trivia?.TriviaAfterUrl ?? StringSlice.Empty; set => Trivia.TriviaAfterUrl = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the GetDynamicUrl delegate. If this property is set,
|
||||
@@ -137,10 +142,9 @@ namespace Markdig.Syntax.Inlines
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the character used to enclose the <see cref="Title"/>.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled.
|
||||
/// </summary>
|
||||
public char TitleEnclosingCharacter { get; set; }
|
||||
public char TitleEnclosingCharacter { get => _trivia?.TitleEnclosingCharacter ?? default; set => Trivia.TitleEnclosingCharacter = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
@@ -150,22 +154,22 @@ 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
|
||||
/// source document including unescaped characters
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
public StringSlice UnescapedTitle { get; set; }
|
||||
public StringSlice UnescapedTitle { get => _trivia?.UnescapedTitle ?? StringSlice.Empty; set => Trivia.UnescapedTitle = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the trivia after the <see cref="Title"/>.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
|
||||
/// <see cref="StringSlice.IsEmpty"/>.
|
||||
/// <see cref="StringSlice.Empty"/>.
|
||||
/// </summary>
|
||||
public StringSlice TriviaAfterTitle { get; set; }
|
||||
public StringSlice TriviaAfterTitle { get => _trivia?.TriviaAfterTitle ?? StringSlice.Empty; set => Trivia.TriviaAfterTitle = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating if this link is a shortcut link to a <see cref="LinkReferenceDefinition"/>
|
||||
@@ -176,5 +180,20 @@ namespace Markdig.Syntax.Inlines
|
||||
/// Gets or sets a boolean indicating whether the inline link was parsed using markdown syntax or was automatic recognized.
|
||||
/// </summary>
|
||||
public bool IsAutoLink { get; set; }
|
||||
|
||||
private sealed class TriviaProperties
|
||||
{
|
||||
public StringSlice LabelWithTrivia;
|
||||
public LocalLabel LocalLabel;
|
||||
public string? LinkRefDefLabel;
|
||||
public StringSlice LinkRefDefLabelWithTrivia;
|
||||
public StringSlice TriviaBeforeUrl;
|
||||
public bool UrlHasPointyBrackets;
|
||||
public StringSlice UnescapedUrl;
|
||||
public StringSlice TriviaAfterUrl;
|
||||
public char TitleEnclosingCharacter;
|
||||
public StringSlice UnescapedTitle;
|
||||
public StringSlice TriviaAfterTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user