Skip to content

Repository files navigation

MarkdigExtensions.Query Logo

MarkdigExtensions.Query 📄🔍

NuGet version

MarkdigExtensions.Query is a powerful, jQuery-style query engine for traversing, filtering, and manipulating Markdown documents in .NET. Built on top of the Markdig Markdown parser, it provides an expressive API for searching and transforming Markdown nodes with CSS-like selectors and familiar LINQ-style chaining.

✨ Features

  • 🔍 jQuery-style Querying: Familiar syntax with CSS selectors and method chaining
  • 🎯 Type-safe Node Access: Strongly-typed access to all Markdown elements
  • 📊 Document Analysis: Built-in statistics, outline generation, and link analysis
  • 🔄 LINQ Integration: Seamless integration with LINQ operations
  • 🌲 DOM-like Traversal: Parent, child, sibling, and ancestor navigation
  • 🎨 CSS Selector Support: Full CSS selector syntax with combinators and pseudo-classes
  • 📈 Performance Optimized: Indexed lookups and efficient querying
  • 🔗 Extension Methods: Easy integration with existing Markdig workflows

🚀 Quick Start

Installation

dotnet add package MarkdigExtensions.Query

Basic Usage

usingMarkdigExtensions.Query;// Convert any markdown string to a queryable documentvarmarkdown="""# Welcome to My DocumentThis is a paragraph with a [link](https://example.com) and **bold text**.## Section 2- Item 1- Item 2 with *emphasis*\```Console.WriteLine("Hello World!");\```| Column 1 | Column 2 ||----------|----------|| Value A | Value B |""";// Create queryable documentvardocument=markdown.AsQueryable();// Now you can query like jQuery!varheadings=document.GetHeadings();varlinks=document.GetLinks();varcodeBlocks=document.GetCodeBlocks();

📖 Core Concepts

Document Conversion

Convert markdown to queryable documents using extension methods:

// From stringvardocument=markdown.AsQueryable();// From Markdig documentvarmarkdigDoc=Markdown.Parse(markdown);vardocument=markdigDoc.AsQueryable();

Element Selection

Access different types of markdown elements:

// Type-based selectionvarheadings=document.GetHeadings();// All headingsvarh1s=document.GetHeadings(1);// Only H1 headingsvarparagraphs=document.GetParagraphs();// All paragraphsvarlinks=document.GetLinks();// All linksvarimages=document.GetImages();// All imagesvarlists=document.GetLists();// All listsvarcodeBlocks=document.GetCodeBlocks();// All code blocksvartables=document.GetTables();// All tablesvartextNodes=document.GetTextNodes();// All text nodes// Generic selection with filteringvarstrongNodes=document.GetNodes<StrongNode>();varemphasizedText=document.GetNodes<EmphasisNode>(e =>e.Children.Any());

🎯 CSS Selector Querying

Use familiar CSS selectors to find elements:

// Basic selectorsvarh1s=document.Query("h1");// All H1 headingsvarlinks=document.Query("link");// All linksvarimages=document.Query("image");// All imagesvarcodeBlocks=document.Query("codeblock");// All code blocks// Attribute selectorsvarcsharpCode=document.Query("codeblock[language=csharp]");varexternalLinks=document.Query("link[url^='https://']");varlevel2Headings=document.Query("heading[level='2']");// CombinatorsvarlinkTexts=document.Query("link text");// Text nodes inside linksvardirectChildren=document.Query("ul > li");// Direct list item childrenvaradjacentSiblings=document.Query("h1 + p");// Paragraphs after H1s// Pseudo-classesvarfirstHeading=document.Query("heading:first");varlastParagraph=document.Query("paragraph:last");varevenItems=document.Query("li:even");varnthChild=document.Query("li:nth-child(2n+1)");// Multiple selectorsvarheadingsAndLinks=document.Query("heading, link");

🌲 DOM-style Traversal

Navigate the document tree like a DOM:

vartextNodes=document.GetTextNodes();// Parent navigationvarparents=textNodes.Parent();// Direct parentsvarancestors=textNodes.Parents();// All ancestorsvarclosestParagraph=textNodes.Closest("paragraph");// Child navigationvarchildren=document.Children();// Direct childrenvardescendants=document.Find("text");// All descendant text nodes// Sibling navigationvarnextSiblings=headings.Next();// Next siblingsvarprevSiblings=headings.Prev();// Previous siblingsvarallNextSiblings=headings.NextAll();// All following siblingsvarallPrevSiblings=headings.PrevAll();// All preceding siblings// Conditional traversalvarnextParagraphs=headings.Next("paragraph");varparentsUntilDocument=textNodes.ParentsUntil(n =>nisDocumentNode);

🔍 Filtering and Selection

Filter and refine your selections:

// Predicate filteringvarlongParagraphs=document.GetParagraphs().Filter(p =>p.Children.Count>10);varexternalLinks=document.GetLinks().Filter(link =>((LinkNode)link).Url?.StartsWith("http")==true);// CSS selector filteringvarh1AndH2=document.GetHeadings().Filter("h1, h2");varnotCodeBlocks=document.Not("codeblock");// Exclusion filteringvarnonEmptyParagraphs=document.GetParagraphs().Not(p =>p.Children.Count==0);// Has filtering (contains descendants)varparagraphsWithLinks=document.GetParagraphs().Has("link");varitemsWithCode=document.Query("li").Has(item =>item.Descendants().Any(d =>disCodeSpanNode));// Is testingboolhasH1=document.Is("h1");boolhasExternalLinks=document.GetLinks().Is(link =>((LinkNode)link).Url?.StartsWith("http")==true);

📍 Index-based Selection

Access elements by position:

varheadings=document.GetHeadings();// Index accessvarfirstHeading=headings.First();// First elementvarlastHeading=headings.Last();// Last elementvarthirdHeading=headings.ElementAt(2);// Zero-based indexvarsecondToLast=headings.ElementAt(^2);// From end// Range slicingvarfirstThree=headings.Slice(0..3);// First 3 elementsvarlastTwo=headings.Slice(^2..);// Last 2 elementsvarmiddle=headings.Slice(1..4);// Elements 1-3varskipTwo=headings.Slice(2);// Skip first 2// Safe accessvarmaybeFirst=headings.FirstOrDefault();// Null if emptyvarmaybeLast=headings.LastOrDefault();// Null if empty

🔄 LINQ Integration and Transformations

Seamlessly integrate with LINQ:

// Transform to other typesvarheadingTexts=document.GetHeadings().Select(h =>((HeadingNode)h).Value).Where(text =>!string.IsNullOrEmpty(text)).ToList();varlinkUrls=document.GetLinks().Select(link =>((LinkNode)link).Url).Where(url =>url?.StartsWith("https://")==true).ToArray();// Complex transformations with indexvarheadingInfo=document.GetHeadings().Select((index,node)=>new{Index=index,Level=((HeadingNode)node).Level,Text=node.Value,Depth=document.GetDepth(node)}).OrderBy(info =>info.Level).ToList();// Iterate with actionsdocument.GetHeadings().Each((index,heading)=>{Console.WriteLine($"Heading {index}: {heading.Value}");});document.GetLinks().Each(link =>{varlinkNode=(LinkNode)link;Console.WriteLine($"Link: {linkNode.Value} -> {linkNode.Url}");});

📊 Document Analysis

Analyze and extract insights from your documents:

Statistics

varstats=document.GetStatistics();// or: var stats = document.GetDocumentStatistics();Console.WriteLine($"Total nodes: {stats["TotalNodes"]}");Console.WriteLine($"Headings: {stats["HeadingCount"]}");Console.WriteLine($"Paragraphs: {stats["ParagraphCount"]}");Console.WriteLine($"Links: {stats["LinkCount"]}");Console.WriteLine($"Images: {stats["ImageCount"]}");Console.WriteLine($"Code blocks: {stats["CodeBlockCount"]}");Console.WriteLine($"Lists: {stats["ListCount"]}");Console.WriteLine($"Tables: {stats["TableCount"]}");Console.WriteLine($"Max depth: {stats["MaxDepth"]}");Console.WriteLine($"Word count: {stats["WordCount"]}");

Document Outline

varoutline=document.GetDocumentOutline();foreach(variteminoutline){varindent=newstring(' ',(item.Level-1)*2);Console.WriteLine($"{indent}- {item.Title} (Level {item.Level})");}

Link Analysis

varlinkAnalysis=document.AnalyzeLinks();foreach(varlinkinlinkAnalysis){Console.WriteLine($"Link: {link.Text}");Console.WriteLine($" URL: {link.Url}");Console.WriteLine($" External: {link.IsExternal}");Console.WriteLine($" Relative: {link.IsRelative}");Console.WriteLine($" Anchor: {link.IsAnchor}");if(link.Title!=null)Console.WriteLine($" Title: {link.Title}");}

🔗 Text Content Extraction

Extract text content from selections:

// Default space separatorvarallText=document.GetTextContent();// Custom separatorvarcommaSeparated=document.GetTextContent(", ");// From specific selectionsvarheadingText=document.GetHeadings().GetTextContent();varparagraphText=document.GetParagraphs().GetTextContent(" | ");// Extract from complex selectionsvarlinkTexts=document.Query("link text").GetTextContent();

🌳 Core Graph Operations

Work with the document's tree structure:

vartextNode=document.GetTextNodes().First().Get()[0];// Tree navigationvarparent=document.GetParent(textNode);varancestors=document.GetAncestors(textNode);vardescendants=MarkdownDocument.GetDescendants(parent);varsiblings=document.GetSiblings(textNode);vardepth=document.GetDepth(textNode);// Tree relationshipsvarallNodes=document.AllNodes;varrootNode=document.Root;

🔄 Method Chaining

Chain operations fluently like jQuery:

// Complex chaining examplevarresult=document.GetHeadings()// Get all headings.Filter(h =>((HeadingNode)h).Level<=3)// Only H1-H3.Not("h1")// Exclude H1s.Parent()// Get their parents.Children("paragraph")// Find paragraph children.Has("link")// That contain links.Each((index,node)=>{// Process eachConsole.WriteLine($"Paragraph {index}: {node.Value}");}).End()// Return to previous selection.Slice(0..5);// Take first 5// Statistical analysis chainvarlinkStats=document.GetLinks().Select(link =>(LinkNode)link).Where(link =>!string.IsNullOrEmpty(link.Url)).GroupBy(link =>link.Url.StartsWith("http")?"External":"Internal").ToDictionary(g =>g.Key, g =>g.Count());

🎨 Advanced Examples

Table of Contents Generation

vartoc=document.GetHeadings().Select(h =>(HeadingNode)h).Select(h =>new{Level=h.Level,Title=h.Value??"",Anchor=h.Value?.ToLower().Replace(" ","-")??""}).ToList();foreach(varitemintoc){varindent=newstring(' ',(item.Level-1)*2);Console.WriteLine($"{indent}- [{item.Title}](#{item.anchor})");}

Link Validation

varbrokenLinks=document.AnalyzeLinks().Where(link =>link.IsExternal).Where(link =>!IsValidUrl(link.Url))// Your validation logic.ToList();foreach(varlinkinbrokenLinks){Console.WriteLine($"Broken link: {link.Text} -> {link.Url}");}

Content Analysis

varanalysis=new{WordCount=(int)document.GetStatistics()["WordCount"],ReadingTime=Math.Ceiling((int)document.GetStatistics()["WordCount"]/200.0),Structure=new{HasToc=document.GetHeadings().Length>3,HasCodeExamples=document.GetCodeBlocks().Length>0,HasTables=document.GetTables().Length>0,HasImages=document.GetImages().Length>0},LinkMetrics=new{Total=document.GetLinks().Length,External=document.AnalyzeLinks().Count(l =>l.IsExternal),Internal=document.AnalyzeLinks().Count(l =>!l.IsExternal&&!l.IsAnchor),Anchors=document.AnalyzeLinks().Count(l =>l.IsAnchor)}};

Document Transformation

// Extract all code examplesvarcodeExamples=document.GetCodeBlocks().Select(cb =>(CodeBlockNode)cb).Where(cb =>!string.IsNullOrEmpty(cb.Language)).GroupBy(cb =>cb.Language).ToDictionary(g =>g.Key, g =>g.Select(cb =>cb.Value).ToList());// Find all TODO items in commentsvartodos=document.GetCodeBlocks().SelectMany(cb =>cb.Value?.Split('\n')??[]).Where(line =>line.Contains("TODO",StringComparison.OrdinalIgnoreCase)).ToList();// Extract definition lists (heading + paragraph patterns)vardefinitions=document.GetHeadings().Where(h =>((HeadingNode)h).Level>=3).Select(h =>new{Term=h.Value,Definition=h.Next("paragraph").GetTextContent()}).Where(d =>!string.IsNullOrEmpty(d.Definition)).ToList();

🛠️ Supported Markdown Elements

Element TypeCSS SelectorType-safe AccessDescription
Headingsh1, h2, h3, h4, h5, h6, headingGetHeadings()All heading levels
Paragraphsparagraph, pGetParagraphs()Text paragraphs
Linkslink, aGetLinks()Hyperlinks
Imagesimage, imgGetImages()Images
Code Blockscodeblock, preGetCodeBlocks()Fenced and indented code
Code SpanscodeGetNodes<CodeSpanNode>()Inline code
Listslist, ul, olGetLists()Ordered and unordered lists
List Itemsli, listitemGetNodes<ListItemNode>()Individual list items
TablestableGetTables()Table structures
Table RowstrGetNodes<TableRowNode>()Table rows
Table Cellstd, thGetNodes<TableCellNode>()Table cells
Emphasisem, emphasisGetNodes<EmphasisNode>()Italic text
StrongstrongGetNodes<StrongNode>()Bold text
BlockquotesblockquoteGetNodes<QuoteBlockNode>()Quote blocks
TexttextGetTextNodes()Raw text content
Line BreaksbrGetNodes<HardLineBreakNode>()Line breaks
Thematic Breakshr, thematicbreakGetNodes<ThematicBreakNode>()Horizontal rules

📚 API Reference

Document Creation

  • string.AsQueryable()
  • MarkdownDocument.AsQueryable()

Selection Methods

  • Query(string selector) - CSS selector query
  • GetHeadings(int? level = null) - Get heading elements
  • GetParagraphs() - Get paragraph elements
  • GetLinks() - Get link elements
  • GetImages() - Get image elements
  • GetCodeBlocks() - Get code block elements
  • GetLists() - Get list elements
  • GetTables() - Get table elements
  • GetTextNodes() - Get text nodes
  • GetNodes<T>(Func<T, bool>? predicate = null) - Generic type-based selection

Filtering Methods

  • Filter(Func<INode, bool> predicate) - Filter by predicate
  • Filter(string selector) - Filter by CSS selector
  • Not(Func<INode, bool> predicate) - Exclude by predicate
  • Not(string selector) - Exclude by CSS selector
  • Has(Func<INode, bool> predicate) - Has descendant matching predicate
  • Has(string selector) - Has descendant matching selector
  • Is(Func<INode, bool> predicate) - Test if any match predicate
  • Is(string selector) - Test if any match selector

Traversal Methods

  • Parent() - Get parent elements
  • Parents() - Get all ancestors
  • Closest(Func<INode, bool> predicate) - Get closest ancestor
  • Children() - Get child elements
  • Find(string selector) - Find descendants
  • Siblings() - Get sibling elements
  • Next() / Prev() - Get adjacent siblings
  • NextAll() / PrevAll() - Get all following/preceding siblings

Index-based Selection

  • First() / Last() - Get first/last element
  • ElementAt(Index index) - Get element at index
  • Slice(Range range) - Get range of elements
  • FirstOrDefault() / LastOrDefault() - Safe access methods

Transformation Methods

  • Select<T>(Func<INode, T> selector) - Transform elements
  • Each(Action<INode> action) - Iterate over elements
  • Get() - Get underlying node collection
  • GetTextContent(string separator = " ") - Extract text content

Analysis Methods

  • GetStatistics() - Get document statistics
  • GetDocumentStatistics() - Alias for GetStatistics
  • GetDocumentOutline() - Get heading-based outline
  • AnalyzeLinks() - Analyze all links in document

Graph Operations

  • GetParent(INode node) - Get parent of specific node
  • GetAncestors(INode node) - Get ancestors of specific node
  • GetDescendants(INode node) - Get descendants of specific node (static)
  • GetSiblings(INode node, bool includeSelf = false) - Get siblings
  • GetDepth(INode node) - Get node depth in tree

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

  • Built on the excellent Markdig library
  • Inspired by jQuery's fluent API design
  • Supports all GitHub Flavored Markdown features

Happy querying! 🎉

About

MarkdigExtensions.Query is a powerful, jQuery-style query engine for traversing, filtering, and manipulating Markdown documents in .NET. Built on top of the Markdig Markdown parser, it provides an expressive API for searching and transforming Markdown nodes with CSS-like selectors.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages