Browse Source

* LaTeX renderer for markdown

Michaël Van Canneyt 1 week ago
parent
commit
a468784364

+ 4 - 3
packages/fcl-md/demo/README.md

@@ -6,8 +6,9 @@ This directory contains 3 markdown parser examples:
 They are:
 
 * demomd - simple markdown parser and html renderer.
-* md2html - slightly more complete markdown-to-html converter/
-* md2fpdoc - simple version of a markdown - to fpdoc converter.
+* md2html - slightly more complete markdown-to-html converter.
+* md2fpdoc - simple version of a markdown to fpdoc converter.
+* md2latex - simple version of a markdown to LaTeX converter.
 
 ## conversion to fpdoc 
 
@@ -24,5 +25,5 @@ The headers determine what is generated for a given section:
 
 links must be rendered as \[text\]\(text\) or \[\]\(text\)
 
-You can find a simple example in the [sample.md](sample.md) file.
+You can find a simple example in the [sampledoc.md](sampledoc.md) file.
 

+ 103 - 0
packages/fcl-md/demo/md2latex.lpi

@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="12"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+        <MainUnitHasScaledStatement Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <Title Value="md2latex"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <CustomData Count="3">
+      <Item0 Name="OpenAPIBase"/>
+      <Item1 Name="OpenAPIConfig"/>
+      <Item2 Name="OpenAPIFile"/>
+    </CustomData>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <Units>
+      <Unit>
+        <Filename Value="md2latex.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="markdown.elements.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="markdown.htmlrender.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="markdown.scanner.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="markdown.utils.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="markdown.parser.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="markdown.inlinetext.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="markdown.render.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="markdown.line.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="markdown.delimiter.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="md2latex"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <OtherUnitFiles Value="../src"/>
+      <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+    <Linking>
+      <Debugging>
+        <DebugInfoType Value="dsDwarf3"/>
+      </Debugging>
+    </Linking>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions>
+      <Item>
+        <Name Value="EAbort"/>
+      </Item>
+      <Item>
+        <Name Value="ECodetoolError"/>
+      </Item>
+      <Item>
+        <Name Value="EFOpenError"/>
+      </Item>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 43 - 0
packages/fcl-md/demo/md2latex.lpr

@@ -0,0 +1,43 @@
+program md2latex;
+
+uses
+  classes,
+  markdown.utils,
+  markdown.elements,
+  markdown.scanner,
+  markdown.parser,
+  markdown.inlinetext,
+  markdown.latexrender,
+  markdown.processors,
+  markdown.render
+  ;
+
+var
+  Source,Dest : TStringList;
+  Doc : TMarkDownDocument;
+
+begin
+  Dest:=Nil;
+  Source:=TStringList.Create;
+  try
+    Dest:=TStringList.Create;
+    Source.LoadFromFile(ParamStr(1));
+    Doc:=TMarkDownParser.FastParse(Source,[]);
+    With TMarkDownLatexRenderer.Create(Nil) do
+      begin
+      Options:=[loEnvelope];
+      If ParamStr(2)='' then
+        Writeln(RenderLaTeX(Doc))
+      else
+        begin
+        Dest:=TStringList.Create;
+        RenderDocument(Doc,Dest);
+        Dest.SaveToFile(ParamStr(2));
+        end;
+      end;
+  finally
+    Source.Free;
+    Dest.Free;
+  end;
+end.
+

+ 8 - 0
packages/fcl-md/fpmake.pp

@@ -90,6 +90,14 @@ begin
       AddUnit('markdown.render');
       end;
 
+    T:=P.Targets.AddUnit('markdown.latexrender.pas');
+    with T.Dependencies do
+      begin
+      AddUnit('markdown.elements');
+      AddUnit('markdown.utils');
+      AddUnit('markdown.render');
+      end;
+
     T:=P.Targets.AddUnit('markdown.fpdocrender.pas');
     with T.Dependencies do
       begin

+ 791 - 0
packages/fcl-md/src/markdown.latexrender.pas

@@ -0,0 +1,791 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown LaTeX renderer.
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ *********************************************************************}
+
+unit MarkDown.LatexRender;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+{$IFDEF FPC_DOTTEDUNITS}
+  System.Classes, System.SysUtils, System.StrUtils, System.Contnrs, 
+{$ELSE}
+  Classes, SysUtils, strutils, contnrs, 
+{$ENDIF}  
+  MarkDown.Elements, 
+  MarkDown.Render, 
+  MarkDown.Utils;
+
+type
+  { TMarkDownLaTeXRenderer }
+  TLaTeXOption = (loEnvelope, loNumberedSections);
+  TLaTeXOptions = set of TLaTeXOption;
+
+  TMarkDownLaTeXRenderer = class(TMarkDownRenderer)
+  private
+    FBuilder: TStringBuilder;
+    FHead: TStrings;
+    FLaTeX: String;
+    FOptions: TLaTeXOptions;
+    FTitle: String;
+    FAuthor: String;
+    procedure SetHead(const aValue: TStrings);
+  Protected
+    Procedure Append(const aContent : String);
+    Procedure AppendNL(const aContent : String = '');
+    Property Builder : TStringBuilder Read FBuilder;
+  public
+    constructor Create(aOwner : TComponent); override;
+    destructor destroy; override;
+    Procedure RenderDocument(aDocument : TMarkDownDocument); override;overload;
+    Procedure RenderDocument(aDocument : TMarkDownDocument; aDest : TStrings); overload;
+    procedure RenderChildren(aBlock : TMarkDownContainerBlock; aAppendNewLine : Boolean); overload;
+    function RenderLaTeX(aDocument : TMarkDownDocument) : string;
+    function EscapeLaTeX(const S: String): String;
+  published
+    Property Options : TLaTeXOptions Read FOptions Write FOptions;
+    property Title : String Read FTitle Write FTitle;
+    property Author : String Read FAuthor Write FAuthor;
+    property Head : TStrings Read FHead Write SetHead;
+  end;
+
+  { TLaTeXMarkDownBlockRenderer }
+
+  TLaTeXMarkDownBlockRenderer = Class (TMarkDownBlockRenderer)
+  Private
+    function GetLaTeXRenderer: TMarkDownLaTeXRenderer;
+  protected
+    procedure Append(const S : String); inline;
+    procedure AppendNl(const S : String = ''); inline;
+    function HasOption(aOption : TLaTeXOption) : Boolean;
+    function Escape(const S: String): String;
+  public
+    property LaTeXRenderer : TMarkDownLaTeXRenderer Read GetLaTeXRenderer;
+  end;
+  TLaTeXMarkDownBlockRendererClass = class of TLaTeXMarkDownBlockRenderer;
+
+  { TLaTeXMarkDownTextRenderer }
+
+  TLaTeXMarkDownTextRenderer = class(TMarkDownTextRenderer)
+  Private
+    FStyleStack: Array of TNodeStyle;
+    FStyleStackLen : Integer;
+    FLastStyles : TNodeStyles;
+    function GetLaTeXRenderer: TMarkDownLaTeXRenderer;
+    function GetNodeTag(aElement: TMarkDownTextNode; Closing: Boolean): string;
+  protected
+    procedure PushStyle(aStyle : TNodeStyle);
+    function PopStyles(aStyle: TNodeStyles): TNodeStyle;
+    procedure PopStyle(aStyle : TNodeStyle);
+    procedure Append(const S : String); inline;
+    procedure DoRender(aElement: TMarkDownTextNode); override;
+    function Escape(const S: String): String;
+    procedure EmitStyleDiff(aStyles : TNodeStyles);
+  Public
+    procedure BeginBlock; override;
+    procedure EndBlock; override;
+    property LaTeXRenderer : TMarkDownLaTeXRenderer Read GetLaTeXRenderer;
+  end;
+  TLaTeXMarkDownTextRendererClass = class of TLaTeXMarkDownTextRenderer;
+
+  { TLaTeXParagraphBlockRenderer }
+
+  TLaTeXParagraphBlockRenderer = class (TLaTeXMarkDownBlockRenderer)
+  protected
+    procedure DoRender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TLaTeXMarkDownQuoteBlockRenderer }
+
+  TLaTeXMarkDownQuoteBlockRenderer = class(TLaTeXMarkDownBlockRenderer)
+  protected
+    procedure dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TLaTeXMarkDownTextBlockRenderer }
+
+  TLaTeXMarkDownTextBlockRenderer = class(TLaTeXMarkDownBlockRenderer)
+  protected
+    procedure DoRender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TLaTeXMarkDownListBlockRenderer }
+
+  TLaTeXMarkDownListBlockRenderer = class(TLaTeXMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TLaTeXMarkDownListItemBlockRenderer }
+
+  TLaTeXMarkDownListItemBlockRenderer = class(TLaTeXMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TLaTeXMarkDownCodeBlockRenderer }
+
+  TLaTeXMarkDownCodeBlockRenderer = class(TLaTeXMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TLaTeXMarkDownHeadingBlockRenderer }
+
+  TLaTeXMarkDownHeadingBlockRenderer = class(TLaTeXMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TLaTeXMarkDownThematicBreakBlockRenderer }
+
+  TLaTeXMarkDownThematicBreakBlockRenderer = class(TLaTeXMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TLaTeXMarkDownTableBlockRenderer }
+
+  TLaTeXMarkDownTableBlockRenderer = class(TLaTeXMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TLaTeXMarkDownTableRowBlockRenderer }
+
+  TLaTeXMarkDownTableRowBlockRenderer = class(TLaTeXMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TLaTeXMarkDownDocumentRenderer }
+
+  TLaTeXMarkDownDocumentRenderer = class(TLaTeXMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+
+implementation
+
+type
+
+  { TStringBuilderHelper }
+
+  TStringBuilderHelper = class helper for TAnsiStringBuilder
+    function Append(aAnsiString : Ansistring) : TAnsiStringBuilder;
+  end;
+
+
+function TStringBuilderHelper.Append(aAnsiString: Ansistring): TAnsiStringBuilder;
+begin
+  Result:=Inherited Append(aAnsiString,0,System.Length(aAnsistring))
+end;
+
+{ TLaTeXMarkDownBlockRenderer }
+
+function TLaTeXMarkDownBlockRenderer.GetLaTeXRenderer: TMarkDownLaTeXRenderer;
+begin
+  if Renderer is TMarkDownLaTeXRenderer then
+    Result:=TMarkDownLaTeXRenderer(Renderer)
+  else
+    Result:=Nil;
+end;
+
+procedure TLaTeXMarkDownBlockRenderer.Append(const S: String);
+begin
+  LaTeXRenderer.Append(S);
+end;
+
+procedure TLaTeXMarkDownBlockRenderer.AppendNl(const S: String);
+begin
+  LaTeXRenderer.AppendNL(S);
+end;
+
+function TLaTeXMarkDownBlockRenderer.HasOption(aOption: TLaTeXOption): Boolean;
+begin
+  Result:=(Self.Renderer is TMarkDownLaTeXRenderer);
+  if Result then
+    Result:=aOption in TMarkDownLaTeXRenderer(Renderer).Options;
+end;
+
+function TLaTeXMarkDownBlockRenderer.Escape(const S: String): String;
+begin
+  Result:=LaTeXRenderer.EscapeLaTeX(S);
+end;
+
+{ TMarkDownLaTeXRenderer }
+
+procedure TMarkDownLaTeXRenderer.SetHead(const aValue: TStrings);
+begin
+  if FHead=aValue then Exit;
+  FHead:=aValue;
+end;
+
+procedure TMarkDownLaTeXRenderer.Append(const aContent: String);
+begin
+  FBuilder.Append(aContent);
+end;
+
+procedure TMarkDownLaTeXRenderer.AppendNL(const aContent: String);
+begin
+  if aContent<>'' then
+    FBuilder.Append(aContent);
+  FBuilder.Append(sLineBreak);
+end;
+
+constructor TMarkDownLaTeXRenderer.Create(aOwner: TComponent);
+begin
+  inherited Create(aOwner);
+  FHead:=TStringList.Create;
+end;
+
+destructor TMarkDownLaTeXRenderer.destroy;
+begin
+  FreeAndNil(FHead);
+  inherited destroy;
+end;
+
+procedure TMarkDownLaTeXRenderer.RenderDocument(aDocument: TMarkDownDocument);
+begin
+  FBuilder:=TStringBuilder.Create;
+  try
+    RenderBlock(aDocument);
+    FLaTeX:=FBuilder.ToString;
+  finally
+    FreeAndNil(FBuilder);
+  end;
+end;
+
+procedure TMarkDownLaTeXRenderer.RenderDocument(aDocument: TMarkDownDocument; aDest: TStrings);
+begin
+  aDest.Text:=RenderLaTeX(aDocument);
+end;
+
+procedure TMarkDownLaTeXRenderer.RenderChildren(aBlock: TMarkDownContainerBlock; aAppendNewLine: Boolean);
+var
+  i,iMax : integer;
+begin
+  iMax:=aBlock.Blocks.Count-1;
+  for I:=0 to iMax do
+    begin
+    if aAppendNewLine and (I>0) then
+      AppendNl();
+    RenderBlock(aBlock.Blocks[I]);
+    end;
+end;
+
+function TMarkDownLaTeXRenderer.RenderLaTeX(aDocument: TMarkDownDocument): string;
+begin
+  RenderDocument(aDocument);
+  Result:=FLaTeX;
+  FLaTeX:='';
+end;
+
+function TMarkDownLaTeXRenderer.EscapeLaTeX(const S: String): String;
+var
+  i: Integer;
+  c: Char;
+begin
+  Result := '';
+  for i := 1 to Length(S) do
+  begin
+    c := S[i];
+    case c of
+      '\': Result := Result + '\textbackslash{}';
+      '{': Result := Result + '\{';
+      '}': Result := Result + '\}';
+      '$': Result := Result + '\$';
+      '&': Result := Result + '\&';
+      '#': Result := Result + '\#';
+      '^': Result := Result + '\textasciicircum{}';
+      '_': Result := Result + '\_';
+      '%': Result := Result + '\%';
+      '~': Result := Result + '\textasciitilde{}';
+    else
+      Result := Result + c;
+    end;
+  end;
+end;
+
+{ TLaTeXMarkDownTextRenderer }
+
+procedure TLaTeXMarkDownTextRenderer.Append(const S: String);
+begin
+  LaTeXRenderer.Append(S);
+end;
+
+function TLaTeXMarkDownTextRenderer.Escape(const S: String): String;
+begin
+  Result:=LaTeXRenderer.EscapeLaTeX(S);
+end;
+
+function TLaTeXMarkDownTextRenderer.GetLaTeXRenderer: TMarkDownLaTeXRenderer;
+begin
+  if Renderer is TMarkDownLaTeXRenderer then
+    Result:=TMarkDownLaTeXRenderer(Renderer)
+  else
+    Result:=Nil;
+end;
+
+function TLaTeXMarkDownTextRenderer.GetNodeTag(aElement: TMarkDownTextNode; Closing: Boolean): string;
+var
+  lUrl: String;
+begin
+  Result := '';
+  case aElement.Kind of
+    nkCode:
+      if Closing then Result := '}' else Result := '\texttt{';
+    nkURI, nkEmail:
+      begin
+        lUrl := '';
+        if aElement.HasAttrs then
+          aElement.Attrs.TryGet('href', lUrl);
+          
+        if Closing then 
+          Result := '}' 
+        else 
+          Result := '\href{' + lUrl + '}{';
+      end;
+    nkImg:
+      begin
+        lUrl := '';
+        if aElement.HasAttrs then
+          aElement.Attrs.TryGet('src', lUrl);
+
+        if Closing then 
+          Result := '}' 
+        else 
+          Result := '\includegraphics{' + lUrl + '}{';
+      end;
+  end;
+end;
+
+procedure TLaTeXMarkDownTextRenderer.PushStyle(aStyle: TNodeStyle);
+begin
+  case aStyle of
+    nsStrong: Append('\textbf{');
+    nsEmph: Append('\textit{');
+    nsDelete: Append('\sout{'); // Requires ulem package
+  end;
+  if FStyleStackLen=Length(FStyleStack) then
+    SetLength(FStyleStack,FStyleStackLen+3);
+  FStyleStack[FStyleStackLen]:=aStyle;
+  Inc(FStyleStackLen);
+end;
+
+function TLaTeXMarkDownTextRenderer.Popstyles(aStyle: TNodeStyles) : TNodeStyle;
+begin
+  if (FStyleStackLen>0) and (FStyleStack[FStyleStackLen-1] in aStyle) then
+    begin
+    Result:=FStyleStack[FStyleStackLen-1];
+    Append('}');
+    Dec(FStyleStackLen);
+    end;
+end;
+
+procedure TLaTeXMarkDownTextRenderer.PopStyle(aStyle: TNodeStyle);
+begin
+  if (FStyleStackLen>0) and (FStyleStack[FStyleStackLen-1]=aStyle) then
+    begin
+    Append('}');
+    Dec(FStyleStackLen);
+    end;
+end;
+
+procedure TLaTeXMarkDownTextRenderer.EmitStyleDiff(aStyles : TNodeStyles);
+var
+  lRemove : TNodeStyles;
+  lAdd : TNodeStyles;
+  S : TNodeStyle;
+begin
+  lRemove:=[];
+  lAdd:=[];
+  For S in TNodeStyle do
+    begin
+    if (S in Self.FLastStyles) and Not (S in aStyles) then
+      Include(lRemove,S);
+    if (S in aStyles) and Not (S in Self.FLastStyles) then
+      Include(lAdd,S);
+    end;
+  While lRemove<>[] do
+    begin
+    S:=Self.PopStyles(lRemove);
+    Exclude(lRemove,S);
+    end;
+  For S in TNodeStyle do
+    if S in lAdd then
+      Self.PushStyle(S);
+  Self.FLastStyles:=aStyles;
+end;
+
+procedure TLaTeXMarkDownTextRenderer.DoRender(aElement: TMarkDownTextNode);
+var
+  lTag : string;
+begin
+  Self.EmitStyleDiff(aElement.Styles);
+  if aElement.Kind <> nkText then
+  begin
+    lTag := Self.GetNodeTag(aElement, False);
+    Append(lTag);
+  end;
+
+  if aElement.Kind = nkImg then
+  begin
+     // Img handling logic here...
+  end;
+
+  if aElement.NodeText<>'' then
+    Append(Self.Escape(aElement.NodeText));
+
+  if aElement.Kind <> nkText then
+  begin
+    lTag := Self.GetNodeTag(aElement, True);
+    Append(lTag);
+  end;
+  
+  aElement.Active:=False;
+end;
+
+procedure TLaTeXMarkDownTextRenderer.BeginBlock;
+begin
+  inherited BeginBlock;
+  Self.FStyleStackLen:=0;
+  Self.FLastStyles:=[];
+end;
+
+procedure TLaTeXMarkDownTextRenderer.EndBlock;
+begin
+  While (Self.FStyleStackLen>0) do
+    Self.Popstyle(Self.FStyleStack[Self.FStyleStackLen-1]);
+  Self.FLastStyles:=[];
+  inherited EndBlock;
+end;
+
+{ TLaTeXParagraphBlockRenderer }
+
+procedure TLaTeXParagraphBlockRenderer.DoRender(aElement: TMarkDownBlock);
+var
+  lNode : TMarkDownParagraphBlock absolute aElement;
+begin
+  // LaTeX paragraphs are separated by blank lines.
+  // No special environment needed usually, unless we want to enforce spacing.
+  Renderer.RenderChildren(lNode);
+  AppendNl; // Blank line after paragraph
+  AppendNl;
+end;
+
+class function TLaTeXParagraphBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownParagraphBlock;
+end;
+
+{ TLaTeXMarkDownTextBlockRenderer }
+
+class function TLaTeXMarkDownTextBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownTextBlock;
+end;
+
+procedure TLaTeXMarkDownTextBlockRenderer.DoRender(aElement: TMarkDownBlock);
+var
+  lNode : TMarkDownTextBlock absolute aElement;
+begin
+  if assigned(lNode) and assigned(lNode.Nodes) then
+    Renderer.RenderTextNodes(lNode.Nodes);
+end;
+
+{ TLaTeXMarkDownQuoteBlockRenderer }
+
+procedure TLaTeXMarkDownQuoteBlockRenderer.dorender(aElement: TMarkDownBlock);
+var
+  lNode : TMarkdownQuoteBlock absolute aElement;
+begin
+  AppendNl('\begin{quote}');
+  Renderer.RenderChildren(lNode);
+  AppendNl('\end{quote}');
+end;
+
+class function TLaTeXMarkDownQuoteBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownQuoteBlock;
+end;
+
+{ TLaTeXMarkDownListBlockRenderer }
+
+procedure TLaTeXMarkDownListBlockRenderer.Dorender(aElement : TMarkDownBlock);
+var
+  lNode : TMarkDownListBlock absolute aElement;
+begin
+  if not lNode.Ordered then
+    AppendNl('\begin{itemize}')
+  else
+    AppendNl('\begin{enumerate}');
+    
+  Renderer.RenderChildren(lNode);
+  
+  if lNode.Ordered then
+    AppendNl('\end{enumerate}')
+  else
+    AppendNl('\end{itemize}');
+end;
+
+class function TLaTeXMarkDownListBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownListBlock;
+end;
+
+
+{ TLaTeXMarkDownListItemBlockRenderer }
+
+procedure TLaTeXMarkDownListItemBlockRenderer.Dorender(aElement : TMarkDownBlock);
+var
+  lItemBlock : TMarkDownListItemBlock absolute aElement;
+  lBlock : TMarkDownBlock;
+  lPar : TMarkDownParagraphBlock absolute lBlock;
+  
+  function IsPlainBlock(aBlock : TMarkDownBlock) : boolean;
+  begin
+    Result:=(aBlock is TMarkDownParagraphBlock)
+             and (aBlock as TMarkDownParagraphBlock).isPlainPara
+             and not (lItemblock.parent as TMarkDownListBlock).loose
+  end;
+
+begin
+  Append('\item ');
+  For lBlock in lItemBlock.Blocks do
+    if IsPlainBlock(lBlock) then
+      LaTeXRenderer.RenderChildren(lPar,True)
+    else
+      Renderer.RenderBlock(lBlock);
+  AppendNl;
+end;
+
+class function TLaTeXMarkDownListItemBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownListItemBlock;
+end;
+
+{ TLaTeXMarkDownCodeBlockRenderer }
+
+procedure TLaTeXMarkDownCodeBlockRenderer.Dorender(aElement : TMarkDownBlock);
+var
+  lNode : TMarkDownCodeBlock absolute aElement;
+  lBlock : TMarkDownBlock;
+begin
+  AppendNl('\begin{verbatim}');
+  for lBlock in LNode.Blocks do
+    begin
+    Renderer.RenderCodeBlock(LBlock,lNode.Lang);
+    AppendNl;
+    end;
+  AppendNl('\end{verbatim}');
+end;
+
+class function TLaTeXMarkDownCodeBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownCodeBlock;
+end;
+
+{ TLaTeXMarkDownThematicBreakBlockRenderer }
+
+procedure TLaTeXMarkDownThematicBreakBlockRenderer.Dorender(aElement : TMarkDownBlock);
+begin
+  if Not Assigned(aElement) then
+    exit;
+  AppendNl('\hrule');
+end;
+
+class function TLaTeXMarkDownThematicBreakBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownThematicBreakBlock;
+end;
+
+{ TLaTeXMarkDownTableBlockRenderer }
+
+procedure TLaTeXMarkDownTableBlockRenderer.Dorender(aElement: TMarkDownBlock);
+var
+  lNode : TMarkDownTableBlock absolute aElement;
+  i : integer;
+  lCols: String;
+  c: TCellAlign;
+begin
+  // Construct column definition
+  lCols := '';
+  for c in lNode.Columns do
+  begin
+    case c of
+      caLeft: lCols := lCols + 'l|';
+      caRight: lCols := lCols + 'r|';
+      caCenter: lCols := lCols + 'c|';
+    end;
+  end;
+  if Length(lCols) > 0 then
+    lCols := '|' + lCols;
+
+  AppendNl('\begin{tabular}{' + lCols + '}');
+  AppendNl('\hline');
+  
+  // Header
+  Renderer.RenderBlock(lNode.blocks[0]);
+  AppendNl('\hline');
+  
+  if lNode.blocks.Count > 1 then
+  begin
+    for i := 1 to lNode.blocks.Count -1  do
+      Renderer.RenderBlock(lnode.blocks[i]);
+    AppendNl('\hline');
+  end;
+  AppendNl('\end{tabular}');
+end;
+
+class function TLaTeXMarkDownTableBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownTableBlock;
+end;
+
+{ TLaTeXMarkDownTableRowBlockRenderer }
+
+procedure TLaTeXMarkDownTableRowBlockRenderer.Dorender(aElement : TMarkDownBlock);
+var
+  lNode : TMarkDownTableRowBlock absolute aElement;
+  i, lCount : integer;
+begin
+  lCount:=lNode.blocks.Count;
+  for i:=0 to lCount-1 do
+    begin
+    if i > 0 then Append(' & ');
+    Renderer.RenderBlock(lNode.blocks[i]);
+    end;
+  AppendNl(' \');
+end;
+
+class function TLaTeXMarkDownTableRowBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownTableRowBlock;
+end;
+
+{ TLaTeXMarkDownHeadingBlockRenderer }
+
+procedure TLaTeXMarkDownHeadingBlockRenderer.Dorender(aElement : TMarkDownBlock);
+var
+  lNode : TMarkDownHeadingBlock absolute aElement;
+  lSection: String;
+  lNumbered: Boolean;
+begin
+  lNumbered := HasOption(loNumberedSections);
+  case lNode.Level of
+    1: lSection := 'section';
+    2: lSection := 'subsection';
+    3: lSection := 'subsubsection';
+    4: lSection := 'paragraph';
+    5: lSection := 'subparagraph';
+    else lSection := 'textbf'; // Fallback
+  end;
+  
+  if not lNumbered then
+    lSection := lSection + '*';
+
+  Append('\' + lSection + '{');
+  Renderer.RenderChildren(lNode);
+  Append('}');
+  AppendNl;
+end;
+
+class function TLaTeXMarkDownHeadingBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownHeadingBlock;
+end;
+
+
+{ TLaTeXMarkDownDocumentRenderer }
+
+procedure TLaTeXMarkDownDocumentRenderer.Dorender(aElement: TMarkDownBlock);
+var
+  H : String;
+begin
+  if HasOption(loEnvelope) then
+    begin
+    AppendNL('\documentclass{article}');
+    AppendNL('\usepackage[utf8]{inputenc}');
+    AppendNL('\usepackage{graphicx}');
+    AppendNL('\usepackage{hyperref}');
+    AppendNL('\usepackage{ulem}'); // For strikethrough
+    
+    if LaTeXRenderer.Title<>'' then
+      AppendNL('\title{' + LaTeXRenderer.EscapeLaTeX(LaTeXRenderer.Title) + '}');
+    if LaTeXRenderer.Author<>'' then
+      AppendNL('\author{' + LaTeXRenderer.EscapeLaTeX(LaTeXRenderer.Author) + '}');
+      
+    for H in LaTeXRenderer.Head do
+      AppendNL(H);
+      
+    AppendNL('\begin{document}');
+    
+    if LaTeXRenderer.Title<>'' then
+      AppendNL('\maketitle');
+    end;
+    
+  Renderer.RenderChildren(aElement as TMarkDownDocument);
+  
+  if HasOption(loEnvelope) then
+    begin
+    AppendNL('\end{document}');
+    end;
+end;
+
+class function TLaTeXMarkDownDocumentRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownDocument
+end;
+
+
+initialization
+  TLaTeXMarkDownHeadingBlockRenderer.RegisterRenderer(TMarkDownLaTeXRenderer);
+  TLaTeXParagraphBlockRenderer.RegisterRenderer(TMarkDownLaTeXRenderer);
+  TLaTeXMarkDownQuoteBlockRenderer.RegisterRenderer(TMarkDownLaTeXRenderer);
+  TLaTeXMarkDownTextBlockRenderer.RegisterRenderer(TMarkDownLaTeXRenderer);
+  TLaTeXMarkDownListBlockRenderer.RegisterRenderer(TMarkDownLaTeXRenderer);
+  TLaTeXMarkDownListItemBlockRenderer.RegisterRenderer(TMarkDownLaTeXRenderer);
+  TLaTeXMarkDownCodeBlockRenderer.RegisterRenderer(TMarkDownLaTeXRenderer);
+  TLaTeXMarkDownThematicBreakBlockRenderer.RegisterRenderer(TMarkDownLaTeXRenderer);
+  TLaTeXMarkDownTableBlockRenderer.RegisterRenderer(TMarkDownLaTeXRenderer);
+  TLaTeXMarkDownTableRowBlockRenderer.RegisterRenderer(TMarkDownLaTeXRenderer);
+  TLaTeXMarkDownDocumentRenderer.RegisterRenderer(TMarkDownLaTeXRenderer);
+  TLaTeXMarkDownTextRenderer.RegisterRenderer(TMarkDownLaTeXRenderer);
+end.

+ 10 - 0
packages/fcl-md/tests/testmd.lpi

@@ -121,6 +121,16 @@
         <IsPartOfProject Value="True"/>
         <UnitName Value="Markdown.FPDocRender"/>
       </Unit>
+      <Unit>
+        <Filename Value="../src/markdown.latexrender.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="MarkDown.LatexRender"/>
+      </Unit>
+      <Unit>
+        <Filename Value="utest.markdown.latexrender.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="UTest.Markdown.LaTeXRender"/>
+      </Unit>
     </Units>
   </ProjectOptions>
   <CompilerOptions>

+ 1 - 1
packages/fcl-md/tests/testmd.lpr

@@ -6,7 +6,7 @@ uses
   cwstring,Classes, consoletestrunner, utest.markdown.utils, markdown.elements, Markdown.HTMLEntities,
   markdown.htmlrender, markdown.inlinetext, markdown.line, markdown.parser, markdown.render, markdown.scanner,
   markdown.utils, utest.markdown.scanner, utest.markdown.inlinetext, utest.markdown.htmlrender, utest.markdown.parser,
-  utest.markdown.fpdocrender,markdown.processors;
+  utest.markdown.fpdocrender,markdown.latexrender,utest.markdown.latexrender,markdown.processors;
 
 type
 

+ 352 - 0
packages/fcl-md/tests/utest.markdown.latexrender.pas

@@ -0,0 +1,352 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown LaTeX renderer tests
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ *********************************************************************}
+unit UTest.Markdown.LaTeXRender;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, fpcunit, testregistry,
+  MarkDown.Elements,
+  MarkDown.LatexRender;
+
+type
+
+  { TTestLaTeXRender }
+
+  TTestLaTeXRender = Class(TTestCase)
+  private
+    FLaTeXRenderer : TMarkDownLaTeXRenderer;
+    FDocument: TMarkDownDocument;
+  Public
+    Procedure SetUp; override;
+    Procedure TearDown; override;
+    function CreateTextBlock(aParent: TMarkdownBlock; const aText,aTextNode: string; aNodeStyle : TNodeStyles=[]): TMarkDownTextBlock;
+    function CreateParagraphBlock(const aTextNode: string): TMarkdownBlock;
+    function CreateQuotedBlock(const aTextNode: string): TMarkdownBlock;
+    function CreateHeadingBlock(const aTextNode: string; aLevel : integer): TMarkdownBlock;
+    function CreateListBlock(aOrdered : boolean; const aListItemText : string): TMarkDownListBlock;
+    function CreateListItemBlock(aParent: TMarkDownContainerBlock; const aText: string): TMarkDownListItemBlock;
+    function AppendTextNode(aBlock: TMarkDownTextBlock; const aText: string; aNodeStyle : TNodeStyles) : TMarkDownTextNode;
+    procedure TestRender(const aLaTeX : string);
+    Property Renderer : TMarkDownLaTeXRenderer Read FLaTeXRenderer;
+    Property Document : TMarkDownDocument Read FDocument;
+  Published
+    procedure TestHookup;
+    procedure TestEmpty;
+    procedure TestEmptyNoEnvelope;
+    procedure TestEmptyTitle;
+    procedure TestTextBlockEmpty;
+    procedure TestTextBlockText;
+    procedure TestTextBlockTextEscaping;
+    procedure TestTextBlockTextStrong;
+    procedure TestTextBlockTextEmph;
+    procedure TestTextBlockTextDelete;
+    procedure TestTextBlockTextStrongEmph;
+    procedure TestPragraphBlockEmpty;
+    procedure TestPragraphBlockText;
+    procedure TestQuotedBlockEmpty;
+    procedure TestQuotedBlockText;
+    procedure TestHeadingBlockEmpty;
+    procedure TestHeadingBlockText;
+    procedure TestHeadingBlockTextLevel2;
+    procedure TestUnorderedListEmpty;
+    procedure TestUnorderedListOneItem;
+  end;
+
+implementation
+
+{ TTestLaTeXRender }
+
+procedure TTestLaTeXRender.SetUp;
+
+begin
+  FLaTeXRenderer:=TMarkDownLaTeXRenderer.Create(Nil);
+  FDocument:=TMarkDownDocument.Create(Nil,1);
+end;
+
+
+procedure TTestLaTeXRender.TearDown;
+
+begin
+  FreeAndNil(FDocument);
+  FreeAndNil(FLaTeXRenderer);
+end;
+
+
+function TTestLaTeXRender.CreateTextBlock(aParent: TMarkdownBlock; const aText, aTextNode: string; aNodeStyle: TNodeStyles): TMarkDownTextBlock;
+
+begin
+  Result:=TMarkDownTextBlock.Create(aParent,1,aText);
+  if aTextNode<>'' then
+    AppendTextNode(Result,aTextNode,aNodeStyle);
+end;
+
+function TTestLaTeXRender.CreateParagraphBlock(const aTextNode: string): TMarkdownBlock;
+
+begin
+  Result:=TMarkDownParagraphBlock.Create(FDocument,1);
+  if aTextNode<>'' then
+    CreateTextBlock(Result,aTextNode,aTextNode);
+end;
+
+function TTestLaTeXRender.CreateQuotedBlock(const aTextNode: string): TMarkdownBlock;
+
+begin
+  Result:=TMarkDownQuoteBlock.Create(FDocument,1);
+  if aTextNode<>'' then
+    CreateTextBlock(Result,aTextNode,aTextNode);
+end;
+
+function TTestLaTeXRender.CreateHeadingBlock(const aTextNode: string; aLevel: integer): TMarkdownBlock;
+
+begin
+  Result:=TMarkDownHeadingBlock.Create(FDocument,1,aLevel);
+  if aTextNode<>'' then
+    CreateTextBlock(Result,aTextNode,aTextNode);
+end;
+
+function TTestLaTeXRender.CreateListItemBlock(aParent: TMarkDownContainerBlock; const aText: string): TMarkDownListItemBlock;
+
+var
+  lPar : TMarkDownParagraphBlock;
+begin
+  Result:=TMarkDownListItemBlock.Create(aParent,1);
+  lPar:=TMarkDownParagraphBlock.Create(Result,1);
+  CreateTextBlock(lPar,'',aText);
+end;
+
+
+function TTestLaTeXRender.CreateListBlock(aOrdered: boolean; const aListItemText: string): TMarkDownListBlock;
+
+begin
+  Result:=TMarkDownListBlock.Create(FDocument,1);
+  Result.ordered:=aOrdered;
+  if aListItemText<>'' then
+    CreateListItemBlock(Result,aListItemText);
+end;
+
+function TTestLaTeXRender.AppendTextNode(aBlock: TMarkDownTextBlock; const aText: string; aNodeStyle: TNodeStyles): TMarkDownTextNode;
+
+var
+  p : TPosition;
+  t : TMarkdownTextNode;
+
+begin
+  if aBlock.Nodes=Nil then
+    aBlock.Nodes:=TMarkDownTextNodeList.Create(True);
+  p.col:=Length(aBlock.Text);
+  p.line:=1;
+  t:=TMarkDownTextNode.Create(p,nkText);
+  t.addText(aText);
+  t.active:=False;
+  T.Styles:=aNodeStyle;
+  aBlock.Nodes.Add(t);
+  Result:=T;
+end;
+
+procedure TTestLaTeXRender.TestRender(const aLaTeX: string);
+
+var
+  L : TStrings;
+
+begin
+  L:=TstringList.Create;
+  try
+    L.SkipLastLineBreak:=True;
+    Renderer.RenderDocument(FDocument,L);
+    assertEquals('Correct latex: ',aLaTeX,L.Text);
+  finally
+    L.Free;
+  end;
+end;
+
+
+procedure TTestLaTeXRender.TestHookup;
+
+begin
+  AssertNotNull('Have renderer',FLaTeXRenderer);
+  AssertNotNull('Have document',FDocument);
+  AssertEquals('Have empty document',0,FDocument.blocks.Count);
+end;
+
+
+procedure TTestLaTeXRender.TestEmpty;
+
+begin
+  Renderer.Options:=[loEnvelope];
+  TestRender('\documentclass{article}'+sLineBreak+
+             '\usepackage[utf8]{inputenc}'+sLineBreak+
+             '\usepackage{graphicx}'+sLineBreak+
+             '\usepackage{hyperref}'+sLineBreak+
+             '\usepackage{ulem}'+sLineBreak+
+             '\begin{document}'+sLineBreak+
+             '\end{document}');
+end;
+
+
+procedure TTestLaTeXRender.TestEmptyNoEnvelope;
+
+begin
+  Renderer.Options:=[];
+  TestRender('');
+end;
+
+
+procedure TTestLaTeXRender.TestEmptyTitle;
+
+begin
+  Renderer.Options:=[loEnvelope];
+  Renderer.Title:='a';
+  TestRender('\documentclass{article}'+sLineBreak+
+             '\usepackage[utf8]{inputenc}'+sLineBreak+
+             '\usepackage{graphicx}'+sLineBreak+
+             '\usepackage{hyperref}'+sLineBreak+
+             '\usepackage{ulem}'+sLineBreak+
+             '\title{a}'+sLineBreak+
+             '\begin{document}'+sLineBreak+
+             '\maketitle'+sLineBreak+
+             '\end{document}');
+end;
+
+
+procedure TTestLaTeXRender.TestTextBlockEmpty;
+
+begin
+  CreateTextBlock(Document,'a','');
+  TestRender('');
+end;
+
+
+procedure TTestLaTeXRender.TestTextBlockText;
+
+begin
+  CreateTextBlock(Document,'a','a');
+  TestRender('a');
+end;
+
+procedure TTestLaTeXRender.TestTextBlockTextEscaping;
+
+begin
+  CreateTextBlock(Document,'a','# $ % ^ & _ { } ~ \');
+  // Expected: \# \$ \% \textasciicircum{} \& \_ \{ \} \textasciitilde{} \textbackslash{}
+  TestRender('\# \$ \% \textasciicircum{} \& \_ \{ \} \textasciitilde{} \textbackslash{}');
+end;
+
+
+procedure TTestLaTeXRender.TestTextBlockTextStrong;
+
+begin
+  CreateTextBlock(Document,'a','a',[nsStrong]);
+  TestRender('\textbf{a}');
+end;
+
+
+procedure TTestLaTeXRender.TestTextBlockTextEmph;
+
+begin
+  CreateTextBlock(Document,'a','a',[nsEmph]);
+  TestRender('\textit{a}');
+end;
+
+
+procedure TTestLaTeXRender.TestTextBlockTextDelete;
+
+begin
+  CreateTextBlock(Document,'a','a',[nsDelete]);
+  TestRender('\sout{a}');
+end;
+
+
+procedure TTestLaTeXRender.TestTextBlockTextStrongEmph;
+
+begin
+  CreateTextBlock(Document,'a','a',[nsStrong,nsEmph]);
+  TestRender('\textbf{\textit{a}}');
+end;
+
+
+procedure TTestLaTeXRender.TestPragraphBlockEmpty;
+
+begin
+  CreateParagraphBlock('');
+  TestRender(sLineBreak); // Blank line
+end;
+
+procedure TTestLaTeXRender.TestQuotedBlockEmpty;
+
+begin
+  CreateQuotedBlock('');
+  TestRender('\begin{quote}'+sLineBreak+'\end{quote}');
+end;
+
+procedure TTestLaTeXRender.TestUnorderedListEmpty;
+
+begin
+  CreateListBlock(false,'');
+  TestRender('\begin{itemize}'+sLineBreak+'\end{itemize}');
+end;
+
+procedure TTestLaTeXRender.TestPragraphBlockText;
+
+begin
+  CreateParagraphBlock('a');
+  TestRender('a'+sLineBreak);
+end;
+
+procedure TTestLaTeXRender.TestQuotedBlockText;
+
+begin
+  CreateQuotedBlock('a');
+  TestRender('\begin{quote}'+sLineBreak+'a\end{quote}');
+end;
+
+procedure TTestLaTeXRender.TestHeadingBlockEmpty;
+
+begin
+  CreateHeadingBlock('',1);
+  TestRender('\section*{}');
+end;
+
+procedure TTestLaTeXRender.TestHeadingBlockText;
+
+begin
+  CreateHeadingBlock('a',1);
+  TestRender('\section*{a}');
+end;
+
+procedure TTestLaTeXRender.TestHeadingBlockTextLevel2;
+
+begin
+  CreateHeadingBlock('a',2);
+  TestRender('\subsection*{a}');
+end;
+
+procedure TTestLaTeXRender.TestUnorderedListOneItem;
+
+begin
+  CreateListBlock(false,'a');
+  // ListItem appends '\item ' then renders children.
+  // Children = Paragraph 'a'. Plain paragraph renders children (text 'a').
+  // Item renderer now adds a newline.
+  // List renderer adds \begin{itemize}\n ... \end{itemize}\n
+  TestRender('\begin{itemize}'+sLineBreak+'\item a'+sLineBreak+'\end{itemize}');
+end;
+
+
+initialization
+  Registertest(TTestLaTeXRender);
+end.