PROWAREtech
.NET: Convert Google's Gemini Markdown Text to HTML
Easily convert Gemini text to HTML for the Web. How to generate Gemini text.
The GeminiTextToHtmlConverter
is a static class that converts text written in a Markdown-like format (called Gemini) into HTML. It provides a single public method ToHtml
that takes a string of Gemini-formatted text and returns the HTML equivalent. The core functionality handles basic text parsing and maintains state for features like code blocks and lists.
The converter supports several formatting features including headings (using = symbols), unordered lists (using - or * markers), ordered lists (using numbers with periods), code blocks (wrapped in triple backticks), and inline formatting like bold (using ** or __) and italic (using * or _) text. It also handles HTML entity escaping for special characters and supports both inline links [text](url) and reference-style links [text][1].
The implementation processes the text line by line, using a StringBuilder for efficient string concatenation. It maintains state variables to track whether it's currently processing a code block or list, ensuring proper HTML tag nesting and closure. The code uses regular expressions for parsing inline formatting and links, and includes helper methods for processing different aspects of the text such as ProcessInlineFormatting
, ProcessLinks
, and EscapeHtml
.
Example Usage
namespace GenerativeAIConsole
{
internal class Program
{
static void Main(string[] args)
{
while (true)
{
Console.Write("Enter question: ");
var line = Console.ReadLine();
if (line == null || line.Length == 0)
break;
try
{
var apiKey = "ENTER_API_KEY_HERE";
var model = new ML.GenAI.GenerativeModel() { ApiKey = apiKey, Model = ML.GenAI.Model.GeminiPro };
var response = model.GenerateContent(line).Result;
string text = string.IsNullOrEmpty(response.Text) ? "**no response**" : response.Text;
Console.WriteLine(ML.GeminiAI.GeminiTextToHtmlConverter.ToHtml(text));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
}
ML.GeminiAI Code
It is a simple class - just one public method!
// GeminiTextParser.cs
using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.FileSystemGlobbing.Internal;
namespace ML.GeminiAI
{
public static class GeminiTextToHtmlConverter
{
private static readonly Dictionary<string, string> HtmlEntities = new()
{
{ "&", "&" },
{ "<", "<" },
{ ">", ">" },
{ "\"", """ },
{ "'", "'" }
};
public static string ToHtml(string textGemini)
{
if (string.IsNullOrEmpty(textGemini))
return string.Empty;
var lines = textGemini.Split('\n');
var htmlBuilder = new StringBuilder();
var inCodeBlock = false;
var inList = false;
var inTable = false;
var listType = string.Empty;
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i];
var trimmedLine = line.Trim();
// Check if we're exiting a list
if (inList && !IsListItem(trimmedLine))
{
htmlBuilder.AppendLine(listType == "ul" ? "</ul>" : "</ol>");
inList = false;
}
// Check if we're exiting a table
if (inTable && !trimmedLine.StartsWith("|"))
{
htmlBuilder.AppendLine("</table>");
inTable = false;
}
// Process each line based on its content
if (line == "" && inCodeBlock)
{
htmlBuilder.AppendLine();
}
else if (line == "")
{
htmlBuilder.AppendLine("<br />");
}
else if(!inCodeBlock && trimmedLine == "---")
{
htmlBuilder.AppendLine("<hr />");
}
else if (!inCodeBlock && trimmedLine.StartsWith("|") && !inTable)
{
inTable = true;
htmlBuilder.AppendLine("<table><tr><td>");
htmlBuilder.AppendLine(ProcessInlineFormatting(EscapeHtml(trimmedLine.Substring(1, trimmedLine.Length - 2))).Replace("|", "</td><td>"));
htmlBuilder.AppendLine("</td></tr>");
}
else if (!inCodeBlock && trimmedLine.StartsWith("|") && inTable)
{
htmlBuilder.AppendLine("<tr><td>");
htmlBuilder.AppendLine(ProcessInlineFormatting(EscapeHtml(trimmedLine.Substring(1, trimmedLine.Length - 2))).Replace("|", "</td><td>"));
htmlBuilder.AppendLine("</td></tr>");
}
else if (!inCodeBlock && trimmedLine.StartsWith("```"))
{
inCodeBlock = true;
htmlBuilder.AppendLine("<pre><code>");
}
else if (inCodeBlock && trimmedLine.StartsWith("```"))
{
htmlBuilder.AppendLine(EscapeHtml(trimmedLine.Substring(3)) + "</code></pre>");
inCodeBlock = false;
}
else if (!inCodeBlock && trimmedLine.StartsWith("#"))
{
var headingLevel = trimmedLine.TakeWhile(c => c == '#').Count();
var headingText = trimmedLine.TrimStart('#').Trim();
htmlBuilder.AppendLine($"<h{headingLevel}>{ProcessInlineFormatting(EscapeHtml(headingText))}</h{headingLevel}>");
}
else if (!inCodeBlock && trimmedLine.StartsWith("="))
{
var headingLevel = trimmedLine.TakeWhile(c => c == '=').Count();
var headingText = trimmedLine.TrimStart('=').Trim();
htmlBuilder.AppendLine($"<h{headingLevel}>{ProcessInlineFormatting(EscapeHtml(headingText))}</h{headingLevel}>");
}
else if (!inCodeBlock && trimmedLine.StartsWith(">"))
{
var headingText = trimmedLine.TrimStart('>').Trim();
htmlBuilder.AppendLine($"<blockquote>{ProcessInlineFormatting(EscapeHtml(headingText))}</blockquote>");
}
else if (!inCodeBlock && IsListItem(trimmedLine))
{
var currentListType = trimmedLine.StartsWith("- ") || trimmedLine.StartsWith("* ") ? "ul" : "ol";
if (!inList)
{
listType = currentListType;
htmlBuilder.AppendLine($"<{listType}>");
inList = true;
}
else if (listType != currentListType)
{
htmlBuilder.AppendLine($"</{listType}>");
listType = currentListType;
htmlBuilder.AppendLine($"<{listType}>");
}
var itemContent = trimmedLine;
if (listType == "ul")
itemContent = trimmedLine.TrimStart('-', '*').Trim();
else
itemContent = Regex.Replace(trimmedLine, @"^\d+\.\s*", "");
htmlBuilder.AppendLine($"<li>{ProcessInlineFormatting(EscapeHtml(itemContent))}</li>");
}
else if (inCodeBlock)
{
htmlBuilder.AppendLine(EscapeHtml(line));
}
else if (!string.IsNullOrWhiteSpace(line))
{
htmlBuilder.AppendLine($"<p>{ProcessInlineFormatting(EscapeHtml(line))}</p>");
}
}
// Close any open tags
if (inCodeBlock)
htmlBuilder.AppendLine("</code></pre>");
if (inTable)
htmlBuilder.AppendLine("</table>");
if (inList)
htmlBuilder.AppendLine(listType == "ul" ? "</ul>" : "</ol>");
return htmlBuilder.ToString().TrimEnd();
}
private static bool IsListItem(string line)
{
return line.StartsWith("- ") || line.StartsWith("* ");
//return line.StartsWith("- ") || line.StartsWith("* ") || Regex.IsMatch(line, @"^\d+\.\s");
}
private static string ProcessInlineFormatting(string text)
{
// Process links first
text = ProcessLinks(text.TrimEnd());
// Process special formatting
text = Regex.Replace(text, @"\*\*(.+?)\*\*", m => $"<strong>{m.Groups[1].Value}{m.Groups[2].Value}</strong>");
text = Regex.Replace(text, @"\*(.+?)\*", m => $"<em>{m.Groups[1].Value}{m.Groups[2].Value}</em>");
text = Regex.Replace(text, @" __(.+?)__ ", m => $"<u> {m.Groups[1].Value}{m.Groups[2].Value} </u>");
text = Regex.Replace(text, @" _(.+?)_ ", m => $"<i> {m.Groups[1].Value}{m.Groups[2].Value} </i>");
text = Regex.Replace(text, @"==(.+?)==", m => $"<mark>{m.Groups[1].Value}{m.Groups[2].Value}</mark>");
string code = @"(?<!\\)`((?:\\.|[^`])*)`";
return Regex.Replace(text, code, m =>
{
string content = m.Groups[1].Value;
return $"<code>{content}</code>";
});
}
private static string ProcessLinks(string text)
{
// Process inline links [text](url)
text = Regex.Replace(text, @"\[([^\]]+)\]\(([^)]+)\)", "<a href=\"$2\">$1</a>");
// Process reference links [text][1]
text = Regex.Replace(text, @"\[([^\]]+)\]\[(\d+)\]", "<a href=\"#ref$2\">$1</a>");
return text;
}
private static string EscapeHtml(string text)
{
foreach (var entity in HtmlEntities)
{
text = text.Replace(entity.Key, entity.Value);
}
return text;
}
}
}