Class MarkdownEditor
- Namespace
- DocToolkit
- Assembly
- DocToolkit.dll
Reads and updates an existing Markdown document — front matter, headings, tables, and one section's content — without converting to another format first.
public static class MarkdownEditor
- Inheritance
-
MarkdownEditor
- Inherited Members
Remarks
No Stream source overload, on any method here. The input is a
string rather than a document, so a caller holding bytes decides their own
encoding — the same reason MarkdownToDocxConverter has no Stream source
overload either.
No async overload either, for a different reason. An overload here is async only where
there is real I/O to await — that is what a Stream overload earns, because draining or
emitting a stream genuinely awaits. Every method on this class is CPU-bound: it parses a string
and returns a value. Wrapping that in a Task would make it
look async without making it so.
No *Core split. That convention exists so a byte[] overload and a
Stream overload cannot drift apart; no method here has more than one overload, so there
is nothing for a split to keep in sync.
No dependency-injection mirror yet, deliberately deferred rather than argued by analogy.
DocToolkit.Extensions.DependencyInjection references the published core package
(see that project's own notes), so the service delegating to this class — the mirror itself —
cannot be implemented before this class has shipped. That is a scheduling constraint, not a
design decision to leave it unmirrored.
Methods
FindHeading(string, string, StringComparison)
The heading in markdown whose text matches headingText,
or null if none does. When more than one heading shares the same text,
the first one in document order is returned.
public static MarkdownHeading? FindHeading(string markdown, string headingText, StringComparison comparison = StringComparison.OrdinalIgnoreCase)
Parameters
markdownstringThe Markdown to search.
headingTextstringThe heading's text to match, without the leading
#markers.comparisonStringComparisonHow
headingTextis compared. Case-insensitive by default.
Returns
Remarks
Every heading is searched, including a nested one — a heading inside a blockquote or a list item is found here like any other. That is deliberately wider than ReplaceSection(string, string, string, StringComparison), which considers top-level headings only because it has to index the document's own block list to find a section's boundaries. Reading a heading has no such constraint, so it does not inherit the restriction.
Exceptions
- ArgumentNullException
markdownorheadingTextis null.- DocumentConversionException
The Markdown could not be parsed.
ReadFrontMatter(string)
Every front-matter key in markdown, with its parsed value. A document
with no front matter returns an empty dictionary, never null.
public static IReadOnlyDictionary<string, object> ReadFrontMatter(string markdown)
Parameters
markdownstringThe Markdown to read.
Returns
Remarks
Values are whatever the underlying reader produced, and not every YAML shape survives.
Every statement below was measured against the pinned OfficeIMO.Markdown 3.2.6 rather
than inferred from YAML in general.
A scalar arrives as one of three runtime types: a quoted or bare word is a
string, a number is a double (never int or long —
version: 3 comes back as 3.0), and true/false is a
bool. An absent value is the empty string: key: with nothing after it,
key: null, key: ~ and key: "" are read as "", "null",
"~" and "" respectively. No value is ever null.
An inline sequence — tags: [alpha, beta] — is a fourth runtime type, a
List<string>. Its items are always strings, so nums: [1, 2] yields
"1" and "2" rather than two doubles, and tags: [] yields
an empty list.
A block sequence — a tags: line with indented - alpha / - beta
items beneath it — is not read. The key is present and maps to an empty string; the
items are lost entirely, with nothing raised. Write the sequence inline if you need to read
it back.
A nested mapping does not nest — it flattens. Given an author: line with
indented name: and email: beneath it, author maps to an empty string
while name and email appear as their own top-level keys of the returned
dictionary; deeper indentation flattens the same way, to the same one level. If a flattened
key collides with another key in the same front matter, only one entry survives and it
carries the later value — silently, exactly as a duplicate top-level key does. A
single-line inline mapping is neither parsed nor flattened: author: {name: Ada}
comes back as the literal string {name: Ada}.
Exceptions
- ArgumentNullException
markdownis null.- DocumentConversionException
The Markdown could not be parsed.
ReadTable(string, int)
The table at index, as rows of cell text — the header row is row 0,
followed by every data row in document order. A row is returned with the shape it has: a
row with fewer or more cells than its neighbours is not padded into a rectangle.
public static IReadOnlyList<IReadOnlyList<string>> ReadTable(string markdown, int index)
Parameters
markdownstringThe Markdown to read.
indexint0-based, indexing what TableCount(string) reports.
Returns
Exceptions
- ArgumentNullException
markdownis null.- ArgumentOutOfRangeException
indexis negative, or at or beyond TableCount(string).- DocumentConversionException
The Markdown could not be parsed.
ReplaceSection(string, string, string, StringComparison)
Replaces the content of the section under the heading matching headingText
with newContent, and returns the whole updated document. Front matter and
every other section are left untouched.
public static string ReplaceSection(string markdown, string headingText, string newContent, StringComparison comparison = StringComparison.OrdinalIgnoreCase)
Parameters
markdownstringThe Markdown to edit.
headingTextstringThe target heading's text, without the leading
#markers, matched against the document's top-level headings only. This is deliberately narrower than FindHeading(string, string, StringComparison), which also matches a heading nested inside a blockquote or a list item — see the remarks for why the two differ.newContentstringThe section's new body, inserted verbatim in place of the blocks between the heading's own line and the start of the next section. Include your own surrounding newlines — this method does not add or normalise whitespace around what you pass. "Blocks" is meant literally: a CommonMark link reference definition leaves no block behind, so one immediately after the heading is kept rather than replaced. See the remarks.
comparisonStringComparisonHow
headingTextis compared. Case-insensitive by default.
Returns
Remarks
A section runs from immediately after the target heading's own line to the start of the next heading at the same or a shallower level (a level-2 target's section can only be closed by another level-1 or level-2 heading, never by a level-3 one), or to the end of the document if there is no such heading.
Line endings that came from markdown are normalised to \n,
and only \r\n is recognised as a line ending here — a lone \r is left exactly
as it is. Normalising is not a stylistic choice: every boundary above is an offset that
OfficeIMO.Markdown computes against LF-normalised text, so splicing those
offsets into an original \r\n string would misalign every one of them by the number of
line breaks preceding it — silently truncating the heading and corrupting an unrelated line
further down. The input is normalised once, then parsed and spliced as one consistent string.
newContent is inserted verbatim and keeps whatever line endings you pass
it, so a \r\n written there survives into the result.
Only top-level headings are ever considered. A heading nested inside a blockquote or a list item is not a candidate — it is not searched, rather than found and then refused — even when no top-level heading shares its text, in which case the call reports that no heading matched. It has to be invisible rather than merely refused: the section boundaries are computed by walking the document's own top-level block list, and searching the whole document first made a legitimate top-level heading uneditable whenever a nested heading earlier in the document happened to share its text.
One measured limitation. A CommonMark link reference definition
([label]: https://example.com) is consumed into the parser's link registry and leaves
no block in the document, so the body's start is found past it. One sitting immediately after
the target heading is therefore not treated as part of the section body and survives
untouched even when the rest of the section is replaced. One sitting after other content in
the same section falls inside the replaced range and does go. Rewriting the splice to track
them was judged the worse trade — the offset arithmetic here is correct and heavily pinned,
and link reference definitions are rare in the section bodies this method exists for.
Exceptions
- ArgumentNullException
Any argument is null.
- ArgumentException
No top-level heading matches
headingText.- DocumentConversionException
The Markdown could not be parsed.
TableCount(string)
The number of tables in markdown, in document order.
public static int TableCount(string markdown)
Parameters
markdownstringThe Markdown to read.
Returns
Exceptions
- ArgumentNullException
markdownis null.- DocumentConversionException
The Markdown could not be parsed.