Is there a "current execution context" available from extensions? #700

Closed
opened 2026-01-29 14:43:20 +00:00 by claunia · 6 comments
Owner

Originally created by @deanebarker on GitHub (Oct 23, 2024).

Does Markdig have a concept of a "current execution context," meaning a common scope available to extensions in which you can store data that can be accessed for that transformation only? (Meaning, the boundaries of a single call to Markdown.ToHtml)

I wrote an extension which allows for the identification of "tokens" (I call them "AtRefs", because of the syntax). They sort of allow the calling of very simple (one parameter) "functions."

For example:

This was defined in a previous article: @article:/path/to/article

In this case, the AtRef is "article" and the parameter is "/path/to/article". It will search the database for that path, and render a hyperlink using the title of the article it finds. I statically register a function on a static class in my extension to handle "article," because this a global scenario that I use all over my site -- every execution of Markdown.ToHtml should have access to this. My extension rendered can get access to that static class and method, so we're good.

But sometimes I want an AtRef handler to only work for a specific execution. This is a handler that is specific to a single block of Markdown -- a "macro," if you will, for that scenario. So, what I need is some data construct accessible to my extension that lives and dies in the context of the single call to Markdown.ToHtml.

This block of Markdown -- this block _only_ -- will do something special with @foo:bar and @foo:baz and @foo:whatever.

Inside my extension renderer (extending from HtmlObjectRenderer<AtRef>), I cannot find anything exposed on which I could bring a custom function in. All the Write method has available is:

  • this, which is the AtRefsRenderer itself -- this is created inside a private method called GetRendererInstance in BaseRenderer; I can't see where I could have get access to this
  • HtmlRenderer -- this comes from a method called RentHtmlRenderer on the pipeline; I can't see where I could get access to this
  • My AtRef object -- this is formed when it's parsed

I thought about somehow passing it in on that last one -- adding it to the AtRef when I parse it. But, that would require the macro to somehow be defined inside the parsed Markdown, which has the same problem: the Match method of InlineParser doesn't seem to have an execution context either.

This is kind of what I want:

var markdownContext = new MarkdownContext() {

  ["AtRef-Foo"] = (a) => { return "Do something..." }

};
var result = Markdown.ToHtml(document, markdownContext);

// markdownContext means nothing, at this point

Then, inside any extension , the MarkdownContext would be available for... whatever. It would only live in the context of that execution.

Either that, or I'd like to be able to extend MarkdownDocument itself:

public class DeanesMarkdownDocument : MarkdownDocument
{
   public Dictionary<string, Func<AtRef, string>> AtRefRenderers = new();
}

var document = Markdown.Parse<DeanesMarkdownDocument>(input, pipeline)
document.AtRefRenders["AtRef-Foo"] =  (a) => { return "Do something..." }

var result = document.ToHtml();

(Of course, right now, extensions don't seem to have access to the larger document, so I'm not sure how this would help me. But it seems somehow... "correct"?)

Does some solution to my problem already exist? Am I overthinking this?

Originally created by @deanebarker on GitHub (Oct 23, 2024). Does Markdig have a concept of a "current execution context," meaning a common scope available to extensions in which you can store data that can be accessed for _that transformation only_? (Meaning, the boundaries of a single call to `Markdown.ToHtml`) I wrote an extension which allows for the identification of "tokens" (I call them "AtRefs", because of the syntax). They sort of allow the calling of very simple (one parameter) "functions." For example: ``` This was defined in a previous article: @article:/path/to/article ``` In this case, the AtRef is "article" and the parameter is "/path/to/article". It will search the database for that path, and render a hyperlink using the title of the article it finds. I statically register a function on a static class in my extension to handle "article," because this a global scenario that I use all over my site -- every execution of `Markdown.ToHtml` should have access to this. My extension rendered can get access to that static class and method, so we're good. But sometimes I want an AtRef handler to _only work for a specific execution_. This is a handler that is specific to a single block of Markdown -- a "macro," if you will, for that scenario. So, what I need is some data construct accessible to my extension that lives and dies in the context of the single call to `Markdown.ToHtml`. ``` This block of Markdown -- this block _only_ -- will do something special with @foo:bar and @foo:baz and @foo:whatever. ``` Inside my extension renderer (extending from `HtmlObjectRenderer<AtRef>`), I cannot find anything exposed on which I could bring a custom function in. All the `Write` method has available is: * `this`, which is the `AtRefsRenderer` itself -- this is created inside a private method called `GetRendererInstance` in `BaseRenderer`; I can't see where I could have get access to this * `HtmlRenderer` -- this comes from a method called `RentHtmlRenderer` on the pipeline; I can't see where I could get access to this * My `AtRef` object -- this is formed when it's parsed I thought about somehow passing it in on that last one -- adding it to the `AtRef` when I parse it. But, that would require the macro to somehow be defined inside the parsed Markdown, which has the same problem: the `Match` method of `InlineParser` doesn't seem to have an execution context either. This is kind of what I want: ``` var markdownContext = new MarkdownContext() { ["AtRef-Foo"] = (a) => { return "Do something..." } }; var result = Markdown.ToHtml(document, markdownContext); // markdownContext means nothing, at this point ``` Then, inside any extension , the `MarkdownContext` would be available for... whatever. It would only live in the context of that execution. Either that, or I'd like to be able to extend `MarkdownDocument` itself: ``` public class DeanesMarkdownDocument : MarkdownDocument { public Dictionary<string, Func<AtRef, string>> AtRefRenderers = new(); } var document = Markdown.Parse<DeanesMarkdownDocument>(input, pipeline) document.AtRefRenders["AtRef-Foo"] = (a) => { return "Do something..." } var result = document.ToHtml(); ``` (Of course, right now, extensions don't seem to have access to the larger document, so I'm not sure how this would help me. But it seems somehow... "correct"?) Does some solution to my problem already exist? Am I overthinking this?
claunia added the question label 2026-01-29 14:43:20 +00:00
Author
Owner

@xoofx commented on GitHub (Oct 24, 2024):

There is indeed no current execution context available across the board.

There is currently one MarkdownParserContext available from parsers.

Otherwise for renderers, each renderer can have its own data/properties (e.g could contain mapping for your functions).

A workaround would be to use a ThreadStatic in your case to unblock if using properties on a custom renderer is not a good solution.

@xoofx commented on GitHub (Oct 24, 2024): There is indeed no current execution context available across the board. There is currently one `MarkdownParserContext` available from parsers. Otherwise for renderers, each renderer can have its own data/properties (e.g could contain mapping for your functions). A workaround would be to use a ThreadStatic in your case to unblock if using properties on a custom renderer is not a good solution.
Author
Owner

@deanebarker commented on GitHub (Oct 24, 2024):

each renderer can have its own data/properties

I like this, but is a renderer instance specific to a single execution? And, if so, how do I get access to it? Where is it "born" in relation to the execution in which it's used?

I traced it back to a collection called ObjectRenderers on RendererBase, but I don't understand how that gets populated in relation to a single execution.

(Note: I also like the ThreadStatic option, but let's pull on the above thread (ha!) first...)

@deanebarker commented on GitHub (Oct 24, 2024): > each renderer can have its own data/properties I like this, but is a renderer instance specific to a single execution? And, if so, how do I get access to it? Where is it "born" in relation to the execution in which it's used? I traced it back to a collection called `ObjectRenderers` on `RendererBase`, but I don't understand how that gets populated in relation to a single execution. (Note: I also like the `ThreadStatic` option, but let's pull on the above thread (ha!) first...)
Author
Owner

@deanebarker commented on GitHub (Oct 24, 2024):

Note that I did get it working using the ThreadStatic method (see image), but it feels... wrong.

Also, ASP.NET will thread switch in the same request, but I don't know when, so it could conceivably switch between me defining the macro and MarkDig rendering. That's probably unlikely, but it could happen.

The "macros" are defined from sections appended to the same document the Markdown appears in (disregard the format; it's a proprietary thing). So @deane:whatever in the document finds the _macro:deane section, parses the content as a Liquid template, then passes whatever to it, and renders it.

Again, this works and proves the theory, but I'd love something more elegant.

image

@deanebarker commented on GitHub (Oct 24, 2024): Note that I did get it working using the `ThreadStatic` method (see image), but it feels... wrong. Also, ASP.NET will thread switch in the same request, but I don't know when, so it could conceivably switch between me defining the macro and MarkDig rendering. That's probably unlikely, but it could happen. The "macros" are defined from sections appended to the same document the Markdown appears in (disregard the format; it's a proprietary thing). So `@deane:whatever` in the document finds the `_macro:deane` section, parses the content as a Liquid template, then passes `whatever` to it, and renders it. Again, this works and proves the theory, but I'd love something more elegant. ![image](https://github.com/user-attachments/assets/d505e2fe-f68b-4a3e-b2e3-3d77128b4614)
Author
Owner

@xoofx commented on GitHub (Oct 24, 2024):

I like this, but is a renderer instance specific to a single execution? And, if so, how do I get access to it? Where is it "born" in relation to the execution in which it's used?

Renderers are configured via pipeline and pipeline builders. There are plenty of examples in the code base and provide custom rendering. For example https://github.com/xoofx/markdig/tree/master/src/Markdig/Extensions/Alerts has a renderer that can define an action per alert kind.

@xoofx commented on GitHub (Oct 24, 2024): > I like this, but is a renderer instance specific to a single execution? And, if so, how do I get access to it? Where is it "born" in relation to the execution in which it's used? Renderers are configured via pipeline and pipeline builders. There are plenty of examples in the code base and provide custom rendering. For example https://github.com/xoofx/markdig/tree/master/src/Markdig/Extensions/Alerts has a renderer that can define an action per alert kind.
Author
Owner

@deanebarker commented on GitHub (Oct 24, 2024):

Ah, okay. I got it working, but I just want to confirm that I'm doing this right.

I created a new "Use" method, so in my pipeline builder, I do this:

var macros = GetMyMacros();
var builder = new MarkdownPipelineBuilder()
    .UseAtRefs(macros)

I created a local field in AtRefsExtension to hold them. So, at this point, the extension itself is holding on to them.

Then, in both the Setup methods, I pass them into the AtRefsInlineParser and AtRefsRenderer instances that those two methods generate and return.

Is that basically right?

@deanebarker commented on GitHub (Oct 24, 2024): Ah, okay. I got it working, but I just want to confirm that I'm doing this right. I created a new "Use" method, so in my pipeline builder, I do this: ``` var macros = GetMyMacros(); var builder = new MarkdownPipelineBuilder() .UseAtRefs(macros) ``` I created a local field in `AtRefsExtension` to hold them. So, at this point, the extension itself is holding on to them. Then, in both the `Setup` methods, I pass them into the `AtRefsInlineParser` and `AtRefsRenderer` instances that those two methods generate and return. Is that basically right?
Author
Owner

@xoofx commented on GitHub (Oct 25, 2024):

Is that basically right?

Yep 😊

@xoofx commented on GitHub (Oct 25, 2024): > Is that basically right? Yep 😊
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/markdig#700