Pārlūkot izejas kodu

* LaTeX renderer for markdown

Michaël Van Canneyt 1 nedēļu atpakaļ
vecāks
revīzija
a468784364

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

@@ -6,8 +6,9 @@ This directory contains 3 markdown parser examples:
 They are:
 They are:
 
 
 * demomd - simple markdown parser and html renderer.
 * 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 
 ## 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\)
 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');
       AddUnit('markdown.render');
       end;
       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');
     T:=P.Targets.AddUnit('markdown.fpdocrender.pas');
     with T.Dependencies do
     with T.Dependencies do
       begin
       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"/>
         <IsPartOfProject Value="True"/>
         <UnitName Value="Markdown.FPDocRender"/>
         <UnitName Value="Markdown.FPDocRender"/>
       </Unit>
       </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>
     </Units>
   </ProjectOptions>
   </ProjectOptions>
   <CompilerOptions>
   <CompilerOptions>

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

@@ -6,7 +6,7 @@ uses
   cwstring,Classes, consoletestrunner, utest.markdown.utils, markdown.elements, Markdown.HTMLEntities,
   cwstring,Classes, consoletestrunner, utest.markdown.utils, markdown.elements, Markdown.HTMLEntities,
   markdown.htmlrender, markdown.inlinetext, markdown.line, markdown.parser, markdown.render, markdown.scanner,
   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,
   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
 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.