A .NET wrapper for mq — a jq-like query tool for Markdown. Query headings, code blocks, paragraphs, and more using a simple, composable query language.
MQNet is a thin managed wrapper around the native mq-ffi library, which is compiled from the mq Rust crate. At runtime, .NET's P/Invoke loads the platform-native binary (mq_ffi.dll / libmq_ffi.so / libmq_ffi.dylib) and marshals calls across the native boundary.
The package ships in two layers:
MQNet— the managed API you reference in your project.MQNet.Runtime.<rid>— thin packages containing only the native binary for a specific platform (e.g.MQNet.Runtime.linux-x64). NuGet's runtime identifier graph selects the right one automatically at restore time.
You only need to install MQNet; the correct native binary is pulled in automatically.
dotnet add package MQNetMQNet uses a split NuGet package model. The main package contains the managed library; native binaries are in separate runtime packages that NuGet resolves automatically based on your platform:
| Platform | Runtime Package |
|---|---|
| Windows x64 | MQNet.Runtime.win-x64 |
| Windows ARM64 | MQNet.Runtime.win-arm64 |
| Linux x64 | MQNet.Runtime.linux-x64 |
| Linux ARM64 | MQNet.Runtime.linux-arm64 |
| macOS x64 | MQNet.Runtime.osx-x64 |
| macOS ARM64 | MQNet.Runtime.osx-arm64 |
using MQNet;
// Fluent API — quick one-shot queries
var result = Mq.Query(".h(1)")
.On("# Hello\n\n## World\n\n# Another")
.Run();
// result[0] → "# Hello"
// result[1] → "# Another"
// result.Text → "# Hello\n# Another"// Extract all H2 headings
var headings = Mq.Query(".h(2)").On(markdown).Run();
// Filter headings containing a word
var filtered = Mq.Query(".h | select(contains(\"API\"))").On(markdown).Run();
// Extract code blocks by language
var rustBlocks = Mq.Query(".code(\"rust\")").On(markdown).Run();
// Query from HTML input
var result = Mq.Query(".h(1)")
.On("<h1>Title</h1><p>Body</p>")
.WithFormat(InputFormat.Html)
.Run();
// Query plain text line by line
var matches = Mq.Query("select(contains(\"error\"))")
.On(logOutput)
.WithFormat(InputFormat.Text)
.Run();Instead of writing raw mq selector strings, use MarkdownTag for strongly-typed, IntelliSense-discoverable selectors:
// Before — raw strings (still supported)
Mq.Query(".h(1)").On(markdown).Run();
Mq.Query(".code(\"rust\")").On(markdown).Run();
// After — typed selectors
Mq.Query(MarkdownTag.H1).On(markdown).Run();
Mq.Query(MarkdownTag.H2).On(markdown).Run();
Mq.Query(MarkdownTag.AllHeadings).On(markdown).Run(); // all headings, any level
Mq.Query(MarkdownTag.Code).On(markdown).Run(); // all code blocks
Mq.Query(MarkdownTag.Link).On(markdown).Run();
Mq.Query(MarkdownTag.List).On(markdown).Run();| Tag | Selector | Description |
|---|---|---|
MarkdownTag.H1 – MarkdownTag.H6 |
.h(1) – .h(6) |
Heading at a specific level |
MarkdownTag.AllHeadings |
.h |
All headings (any level) |
MarkdownTag.Paragraph / MarkdownTag.Text |
.text |
Paragraph / text nodes |
MarkdownTag.Code |
.code |
All fenced code blocks |
MarkdownTag.InlineCode |
.code_inline |
Inline code spans |
MarkdownTag.Link |
.link |
Link nodes |
MarkdownTag.Image |
.image |
Image nodes |
MarkdownTag.List |
.list |
List items |
MarkdownTag.Blockquote |
.blockquote |
Block quotes |
MarkdownTag.Table |
.table |
Tables |
MarkdownTag.HorizontalRule |
.horizontal_rule |
Horizontal rules |
MarkdownTag.LineBreak |
.break |
Line breaks |
MarkdownTag.Footnote |
.footnote |
Footnotes |
MarkdownTag.MathInline |
.math_inline |
Inline math |
MarkdownTag.Html |
.html |
Raw HTML nodes |
// Language-filtered code blocks
Mq.Query(MarkdownTag.CodeBlock("rust")).On(markdown).Run();
// Heading at a specific level (1–6)
Mq.Query(MarkdownTag.HeadingLevel(3)).On(markdown).Run();
// Heading range — inclusive (both ends included)
Mq.Query(MarkdownTag.HeadingRange(1, 3)).On(markdown).Run(); // H1, H2, H3
// Heading range — C# Range syntax (exclusive end, follows C# Range convention)
Mq.Query(MarkdownTag.Heading(1..3)).On(markdown).Run(); // H1, H2 (NOT H3 — exclusive end)
Mq.Query(MarkdownTag.Heading(1..)).On(markdown).Run(); // H1–H6 (open end)
Mq.Query(MarkdownTag.Heading(^3..)).On(markdown).Run(); // H3–H6 (from-end index)Range semantics note:
HeadingRange(from, to)uses inclusive bounds —HeadingRange(1, 3)selects H1, H2, and H3.Heading(Range)follows standard C# Range semantics with an exclusive end —Heading(1..3)selects H1 and H2 only.
// Shorthand for Mq.Query(MarkdownTag.HeadingLevel(1))
Mq.Heading(1).On(markdown).Run();
// Shorthand for Mq.Query(MarkdownTag.CodeBlock("rust"))
Mq.CodeBlock("rust").On(markdown).Run();using var engine = new MqEngine();
var h1 = engine.Eval(".h(1)", markdown);
var code = engine.Eval(".code", markdown);
var links = engine.Eval(".link", markdown);// Basic conversion
string markdown = MqEngine.HtmlToMarkdown("<h1>Hello</h1><p>World</p>");
// With options
string markdown = MqEngine.HtmlToMarkdown(html, new ConversionOptions
{
UseTitleAsH1 = true,
GenerateFrontMatter = true,
ExtractScriptsAsCodeBlocks = true
});MqResult result = Mq.Query(".h").On(markdown).Run();
result.Count // number of matches
result[0] // first match
result.Values // IReadOnlyList<string>
result.Text // all matches joined by "\n"
foreach (var item in result)
Console.WriteLine(item);mq has a built-in to_text() function that strips Markdown formatting at the AST level. Use it by piping your query through to_text, or use the WithPlainText() convenience method on the fluent API:
// Fluent API — WithPlainText() appends "| to_text" to the query
var result = Mq.Query(".h(1)")
.On("# Hello **World**\n\n## Section")
.WithPlainText()
.Run();
// result[0] → "Hello World" (no # or ** markers)
// Equivalent using MqEngine directly
using var engine = new MqEngine();
var result = engine.Eval(".h | to_text", "# Hello\n\n## World");
// result.Values → ["Hello", "World"]mq ships 15 standard modules embedded in the native binary. Load them with LoadModule (global scope) or ImportModule (namespaced) on an MqEngine instance — no file paths required.
using var engine = new MqEngine();
// LoadModule puts functions in global scope
engine.LoadModule("json");
var result = engine.Eval("""
.code("json") | to_text | json_parse | json_stringify
""", markdown);
// ImportModule requires a namespace prefix
engine.ImportModule("semver");
var ok = engine.Eval(
"""semver::semver_satisfies("1.5.0", ">=1.0.0,<2.0.0")""",
"ignored", InputFormat.Text);
// → ["true"]| Module | Load name | Key functions |
|---|---|---|
| md | "md" |
Build Markdown nodes — h(), code(), text(), strong(), em(), link(), image(), list(), table_row(), doc() |
| section | "section" |
Structure by headings — sections(), section(), title_contains(), body(), toc(), collect(), split(), by_level() |
| json | "json" |
json_parse(), json_stringify(), json_to_markdown_table() |
| yaml | "yaml" |
yaml_parse(), yaml_stringify(), yaml_to_markdown_table(), to_frontmatter() |
| toml | "toml" |
toml_parse(), toml_stringify(), toml_to_json(), toml_to_markdown_table() |
| csv | "csv" |
csv_parse(), tsv_parse(), psv_parse(), csv_stringify(), csv_to_markdown_table() |
| xml | "xml" |
xml_parse(), xml_stringify(), xml_to_markdown_table() |
| hcl | "hcl" |
hcl_parse(), hcl_stringify() — HashiCorp Configuration Language |
| semver | "semver" |
semver_parse(), semver_compare(), semver_gt/lt/eq/gte/lte(), semver_sort(), semver_bump_major/minor/patch(), semver_satisfies() |
| table | "table" |
Structured Markdown table manipulation — tables(), add_row(), add_column(), filter_rows(), sort_rows(), to_csv() |
| fuzzy | "fuzzy" |
Fuzzy string matching — levenshtein(), jaro(), jaro_winkler(), fuzzy_match(), fuzzy_filter() |
| cbor | "cbor" |
cbor_parse() (base64 or raw bytes), cbor_stringify() |
| toon | "toon" |
TOON format — toon_parse(), toon_stringify() |
| ast | "ast" |
mq AST introspection — get_args(), to_code() |
| test | "test" |
Testing framework — assert_eq(), assert_true(), run_tests(), test_case() |
Parse a JSON code block and reformat it:
using var engine = new MqEngine();
engine.LoadModule("json");
string markdown = """
# Config
```json
{"name":"acme","version":"1.0.0"}
```
""";
// Extract the JSON code block, parse it, then stringify
var result = engine.Eval(
""".code("json") | to_text | json_parse | json_stringify""",
markdown);
// result[0] → "{\"name\": \"acme\", \"version\": \"1.0.0\"}"Collect all headings as a table of contents:
using var engine = new MqEngine();
engine.LoadModule("section");
var toc = engine.Eval("sections(.) | toc", markdown);
// result → ["- Introduction", " - Background", "- Usage", ...]Check if a version matches a range:
using var engine = new MqEngine();
engine.ImportModule("semver");
var result = engine.Eval(
"""semver::semver_satisfies("2.3.1", ">=2.0.0,<3.0.0")""",
"ignored", InputFormat.Text);
// result[0] → "true"Build Markdown programmatically:
using var engine = new MqEngine();
engine.LoadModule("md");
var result = engine.Eval(
"""doc(h("Hello", 1), text("World"), code("let x = 1;", "rust"))""",
"ignored", InputFormat.Text);
// result[0] → "# Hello\n\nWorld\n\n```rust\nlet x = 1;\n```"Load .mq files from the file system with SetSearchPaths:
using var engine = new MqEngine();
engine.SetSearchPaths(["/path/to/my/modules"]);
// Load myutils.mq — functions available in global scope
engine.LoadModule("myutils");
// Import myutils.mq — functions available as myutils::fn_name()
engine.ImportModule("myutils");| Format | Description |
|---|---|
InputFormat.Markdown |
CommonMark / GFM Markdown (default) |
InputFormat.Mdx |
Markdown with JSX (MDX) |
InputFormat.Html |
HTML — auto-converted to Markdown before querying |
InputFormat.Text |
Plain text, split by lines |
InputFormat.Raw |
Raw string, no parsing |
- .NET 8 or .NET 10
- Supported platforms: Windows x64/ARM64, Linux x64/ARM64, macOS x64/ARM64
MQNet wraps the mq Rust library. For the full query language reference, see the mq documentation.
Some common queries:
.h # all headings
.h(1) # H1 headings only
.h(2) # H2 headings only
.code # all code blocks
.code("go") # code blocks with language "go"
.text # paragraphs / text nodes
.link # links
.image # images
.list # list items
# Combinators
.h | select(contains("API")) # headings containing "API"
.h | map(ascii_downcase) # lowercase all headings
.code | select(startswith("fn ")) # code blocks starting with "fn "
# Plain text (strip Markdown formatting)
.h | to_text # heading text without # markers
.h(1) | to_text # H1 text only, no formatting
MIT — see LICENSE.
This project wraps mq by harehare, which is also MIT licensed.