Browse Source

* Initial markdown parser

Michaël Van Canneyt 2 weeks ago
parent
commit
247d777d5e
36 changed files with 12395 additions and 11 deletions
  1. 2 0
      packages/fcl-md/Makefile
  2. 20 0
      packages/fcl-md/README.md
  3. 28 0
      packages/fcl-md/demo/README.md
  4. 97 0
      packages/fcl-md/demo/demomd.lpi
  5. 40 0
      packages/fcl-md/demo/demomd.lpr
  6. 62 0
      packages/fcl-md/demo/md2fpdoc.lpi
  7. 178 0
      packages/fcl-md/demo/md2fpdoc.lpr
  8. 62 0
      packages/fcl-md/demo/md2html.lpi
  9. 177 0
      packages/fcl-md/demo/md2html.lpr
  10. 16 0
      packages/fcl-md/demo/sampledoc.md
  11. 112 0
      packages/fcl-md/fpmake.pp
  12. 617 0
      packages/fcl-md/src/markdown.elements.pas
  13. 917 0
      packages/fcl-md/src/markdown.fpdocrender.pas
  14. 2160 0
      packages/fcl-md/src/markdown.htmlentities.pas
  15. 773 0
      packages/fcl-md/src/markdown.htmlrender.pas
  16. 984 0
      packages/fcl-md/src/markdown.inlinetext.pas
  17. 179 0
      packages/fcl-md/src/markdown.line.pas
  18. 816 0
      packages/fcl-md/src/markdown.parser.pas
  19. 1152 0
      packages/fcl-md/src/markdown.processors.pas
  20. 513 0
      packages/fcl-md/src/markdown.render.pas
  21. 348 0
      packages/fcl-md/src/markdown.scanner.pas
  22. 724 0
      packages/fcl-md/src/markdown.utils.pas
  23. 1 0
      packages/fcl-md/tests/README.md
  24. 156 0
      packages/fcl-md/tests/testmd.lpi
  25. 31 0
      packages/fcl-md/tests/testmd.lpr
  26. 390 0
      packages/fcl-md/tests/utest.markdown.fpdocrender.pas
  27. 372 0
      packages/fcl-md/tests/utest.markdown.htmlrender.pas
  28. 285 0
      packages/fcl-md/tests/utest.markdown.inlinetext.pas
  29. 399 0
      packages/fcl-md/tests/utest.markdown.parser.pas
  30. 281 0
      packages/fcl-md/tests/utest.markdown.scanner.pas
  31. 297 0
      packages/fcl-md/tests/utest.markdown.utils.pas
  32. 6 0
      packages/fcl-md/tools/README.md
  33. 56 0
      packages/fcl-md/tools/json2entities.lpi
  34. 126 0
      packages/fcl-md/tools/json2entities.lpr
  35. 11 10
      packages/fpmake_add.inc
  36. 7 1
      packages/fpmake_proc.inc

+ 2 - 0
packages/fcl-md/Makefile

@@ -0,0 +1,2 @@
+PACKAGE_NAME=fcl-md
+include ../build/Makefile.pkg

+ 20 - 0
packages/fcl-md/README.md

@@ -0,0 +1,20 @@
+
+This directory contains an extensible markdown parser.
+
+Extensible means 2 things:
+
+* You can add new block types by simply registering a processor for it.
+  You can also override a block type's parser to provide custom behaviour.
+
+* You can render the markdown to whatever format you want. 
+  By default, rendering to html and fpdoc are supported. (LaTeX still planned)
+
+Both renderers are demoed in the [demo](demo) directory.
+
+While the commonmark spec has been used in the implementation, 
+the parser makes no pretence at being fully commonmark compliant.
+
+In particular, it should parse whatever commonmark defines, but the html
+output may not be 100% the same, as the commonmark spec is overly pedantic
+where it concerns whitespace usage in the output.
+(although patches to improve the output are welcome)

+ 28 - 0
packages/fcl-md/demo/README.md

@@ -0,0 +1,28 @@
+# Markdown parser examples
+
+## Synopsis
+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.
+
+## conversion to fpdoc 
+
+The headers determine what is generated for a given section:
+
+* Level 1 - contains the unit name.
+* Level 2 - Start Element or Topic (use "topic: name")
+* Level 3 - Parts of element/topic. Must have one of the following titles
+  * short
+  * descr or description
+  * errors
+  * seealso
+  * example or examples
+
+links must be rendered as \[text\]\(text\) or \[\]\(text\)
+
+You can find a simple example in the [sample.md](sample.md) file.
+

+ 97 - 0
packages/fcl-md/demo/demomd.lpi

@@ -0,0 +1,97 @@
+<?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="demomd"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <Units>
+      <Unit>
+        <Filename Value="demomd.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="demomd"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <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>

+ 40 - 0
packages/fcl-md/demo/demomd.lpr

@@ -0,0 +1,40 @@
+program demomd;
+
+uses
+  classes,
+  markdown.utils,
+  markdown.elements,
+  markdown.scanner,
+  markdown.parser,
+  markdown.inlinetext,
+  markdown.htmlrender,
+  markdown.render, markdown.line, markdown.delimiter;
+
+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 TMarkDownHTMLRenderer.Create(Nil) do
+      begin
+      If ParamStr(2)='' then
+        Writeln(RenderHTML(Doc))
+      else
+        begin
+        Dest:=TStringList.Create;
+        RenderDocument(Doc,Dest);
+        Dest.SaveToFile(ParamStr(2));
+        end;
+      end;
+  finally
+    Source.Free;
+    Dest.Free;
+  end;
+end.
+

+ 62 - 0
packages/fcl-md/demo/md2fpdoc.lpi

@@ -0,0 +1,62 @@
+<?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="Markdown to HTML converter"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <Units>
+      <Unit>
+        <Filename Value="md2fpdoc.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="md2fpdoc"/>
+    </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>

+ 178 - 0
packages/fcl-md/demo/md2fpdoc.lpr

@@ -0,0 +1,178 @@
+program md2fpdoc;
+
+{$mode objfpc}{$H+}
+
+uses
+  {$IFDEF UNIX}
+  cthreads,
+  {$ENDIF}
+  Classes, SysUtils, CustApp, Markdown.Processors, MarkDown.Elements, Markdown.FPDocRender, MarkDown.Parser;
+
+type
+
+  { TMD2HTMLApplication }
+
+  TMD2HTMLApplication = class(TCustomApplication)
+  const
+    ShortOptions = 'hi:o:p:q';
+    LongOptions : Array of String = ('help','input:','output:','package:','quiet');
+  private
+    FQuiet : Boolean;
+    FOutput : String;
+    FInputs : Array of string;
+    FPackage : String;
+    procedure DoLog(EventType: TEventType; const Msg: String); override;
+    procedure ConvertMarkDown(const aInput, aOutput: string);
+    function CreateOutputFileName(const aInput: string; isMulti: boolean): string;
+  protected
+    procedure DoRun; override;
+    function ProcessOptions : boolean; virtual;
+    procedure Usage(const ErrMsg: string); virtual;
+  public
+    constructor Create(aOwner: TComponent); override;
+    destructor Destroy; override;
+  end;
+
+{ TMD2HTMLApplication }
+
+function TMD2HTMLApplication.CreateOutputFileName(const aInput : string; isMulti : boolean) : string;
+var
+  lDir : string;
+begin
+  if isMulti then
+    begin
+    lDir:=Foutput;
+    if lDir<>'' then
+      lDir:=IncludeTrailingPathDelimiter(lDir);
+    Result:=lDir+ChangeFileExt(ExtractFileName(aInput),'.xml');
+    end
+  else if FOutput<>'' then
+    Result:=FOutput
+  else
+    Result:=ChangeFileExt(aInput,'.xml');
+end;
+
+procedure TMD2HTMLApplication.DoLog(EventType: TEventType; const Msg: String);
+begin
+  if FQuiet then
+    exit;
+  Writeln(StdErr,'[',EventType,'] ',Msg);
+end;
+
+procedure TMD2HTMLApplication.ConvertMarkDown(const aInput,aOutput : string);
+var
+  lRenderer : TMarkDownFPDocRenderer;
+  lParser : TMarkDownParser;
+  lDoc : TMarkDownDocument;
+  lMarkDown,lHTML : TStrings;
+
+begin
+  Log(etInfo,'Converting %s to %s',[aInput,aOutput]);
+  try
+    lParser:=Nil;
+    lDoc:=Nil;
+    lMarkDown:=TStringList.Create;
+    try
+      lMarkDown.LoadFromFile(aInput);
+      lParser:=TMarkDownParser.Create(Self);
+      lDoc:=lParser.Parse(lMarkDown);
+      lRenderer:=TMarkDownFPDocRenderer.Create(Self);
+      lRenderer.PackageName:=FPackage;
+      lHTML:=TStringList.Create;
+      lRenderer.RenderDocument(lDoc,lHTML);
+      lHTML.SaveToFile(aOutput);
+    finally
+      lHTML.Free;
+      lRenderer.Free;
+      lDoc.Free;
+      lParser.Free;
+      lMarkDown.Free;
+    end;
+  except
+    on E : Exception do
+      Log(etError,'Error %s while onverting %s to %s : %s',[E.ClassName,aInput,aOutput,E.Message]);
+  end;
+end;
+
+procedure TMD2HTMLApplication.DoRun;
+var
+  ErrorMsg: String;
+  lInPut,lOutput : String;
+begin
+  Terminate;
+  ErrorMsg:=CheckOptions(ShortOptions,LongOptions);
+  if (ErrorMsg<>'') or HasOption('h','help') then
+    begin
+    Usage(ErrorMsg);
+    Exit;
+    end;
+  if not ProcessOptions then
+    Exit;
+  For lInput in FInputs do
+    begin
+    lOutput:=CreateOutputFileName(lInput,Length(FInputs)>1);
+    ConvertMarkDown(lInput,lOutput);
+    end;
+end;
+
+function TMD2HTMLApplication.ProcessOptions: boolean;
+
+begin
+  Result:=False;
+  FQuiet:=HasOption('q','quiet');
+  FInputs:=GetOptionValues('i','input');
+  if Length(FInputs)=0 then
+    FInputs:=GetNonOptions(ShortOptions,LongOptions);
+  FOutput:=GetOptionValue('o','output');
+  if Length(FInputs)>1 then
+    If not DirectoryExists(FOutput) then
+      begin
+      Usage('Directory does not exist or is not a directory: '+Foutput);
+      exit;
+      end;
+  FPackage:=GetOptionValue('p','package');
+  if FPackage='' then
+    begin
+    Usage('A package name is required');
+    exit;
+    end;
+  Result:=True;
+end;
+
+constructor TMD2HTMLApplication.Create(aOwner: TComponent);
+begin
+  inherited Create(aOwner);
+  StopOnException:=True;
+end;
+
+destructor TMD2HTMLApplication.Destroy;
+begin
+  inherited Destroy;
+end;
+
+procedure TMD2HTMLApplication.Usage(const ErrMsg : string);
+begin
+  if ErrMsg<>'' then
+    begin
+    Writeln(StdErr,'Error: ',ErrMsg);
+    Flush(StdErr);
+    end;
+  Writeln('Usage: ', ExeName, ' [options]');
+  Writeln('Where options is one or more of:');
+  Writeln('-h --help         this message.');
+  Writeln('-i --input=FILE   Input markdown file.');
+  Writeln('-o --output=FILE  Output HTML file.');
+  Writeln('-p --package=NAME Set package name.');
+  Writeln('-q --quiet        Less messages.');
+  ExitCode:=Ord(ErrMsg<>'');
+end;
+
+var
+  Application: TMD2HTMLApplication;
+begin
+  Application:=TMD2HTMLApplication.Create(nil);
+  Application.Title:='Markdown to FPDoc converter';
+  Application.Run;
+  Application.Free;
+end.
+

+ 62 - 0
packages/fcl-md/demo/md2html.lpi

@@ -0,0 +1,62 @@
+<?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="Markdown to HTML converter"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <Units>
+      <Unit>
+        <Filename Value="md2html.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="md2html"/>
+    </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>

+ 177 - 0
packages/fcl-md/demo/md2html.lpr

@@ -0,0 +1,177 @@
+program md2html;
+
+{$mode objfpc}{$H+}
+
+uses
+  {$IFDEF UNIX}
+  cthreads,
+  {$ENDIF}
+  Classes, SysUtils, CustApp, Markdown.Processors, MarkDown.Elements, Markdown.HtmlRender, MarkDown.Parser;
+
+type
+
+  { TMD2HTMLApplication }
+
+  TMD2HTMLApplication = class(TCustomApplication)
+  const
+    ShortOptions = 'hfi:o:H:t:';
+    LongOptions : Array of String = ('help','full','input:','output:','head:','title:');
+  private
+    FHead : TStrings;
+    FOutput : String;
+    FInputs : Array of string;
+    FHTMLTitle : String;
+    FFull : Boolean;
+    procedure ConvertMarkDown(const aInput, aOutput: string);
+    function CreateOutputFileName(const aInput: string; isMulti: boolean): string;
+  protected
+    procedure DoRun; override;
+    function ProcessOptions : boolean; virtual;
+    procedure Usage(const ErrMsg: string); virtual;
+  public
+    constructor Create(aOwner: TComponent); override;
+    destructor Destroy; override;
+  end;
+
+{ TMD2HTMLApplication }
+
+function TMD2HTMLApplication.CreateOutputFileName(const aInput : string; isMulti : boolean) : string;
+var
+  lDir : string;
+begin
+  if isMulti then
+    begin
+    lDir:=Foutput;
+    if lDir<>'' then
+      lDir:=IncludeTrailingPathDelimiter(lDir);
+    Result:=lDir+ChangeFileExt(ExtractFileName(aInput),'.html');
+    end
+  else if FOutput<>'' then
+    Result:=FOutput
+  else
+    Result:=ChangeFileExt(aInput,'.html');
+  Writeln(aInput,' -> ',Result);
+end;
+
+procedure TMD2HTMLApplication.ConvertMarkDown(const aInput,aOutput : string);
+var
+  lRenderer : TMarkDownHTMLRenderer;
+  lParser : TMarkDownParser;
+  lDoc : TMarkDownDocument;
+  lMarkDown,lHTML : TStrings;
+
+begin
+  lParser:=Nil;
+  lDoc:=Nil;
+  lMarkDown:=TStringList.Create;
+  try
+    lMarkDown.LoadFromFile(aInput);
+    lParser:=TMarkDownParser.Create(Self);
+    lDoc:=lParser.Parse(lMarkDown);
+    lRenderer:=TMarkDownHTMLRenderer.Create(Self);
+    if FFull then
+      begin
+      lRenderer.Options:=[hoEnvelope,hoHead];
+      lRenderer.Title:=FHTMLTitle;
+      if assigned(FHead) then
+        lRenderer.Head:=FHead;
+      end;
+   lHTML:=TStringList.Create;
+   lRenderer.RenderDocument(lDoc,lHTML);
+   lHTML.SaveToFile(aOutput);
+  finally
+    lHTML.Free;
+    lRenderer.Free;
+    lDoc.Free;
+    lParser.Free;
+    lMarkDown.Free;
+  end;
+end;
+
+procedure TMD2HTMLApplication.DoRun;
+var
+  ErrorMsg: String;
+  lInPut,lOutput : String;
+begin
+  Terminate;
+  ErrorMsg:=CheckOptions(ShortOptions,LongOptions);
+  if (ErrorMsg<>'') or HasOption('h','help') then
+    begin
+    Usage(ErrorMsg);
+    Exit;
+    end;
+  if not ProcessOptions then
+    Exit;
+  For lInput in FInputs do
+    begin
+    lOutput:=CreateOutputFileName(lInput,Length(FInputs)>1);
+    ConvertMarkDown(lInput,lOutput);
+    end;
+end;
+
+function TMD2HTMLApplication.ProcessOptions: boolean;
+var
+  lFileName : string;
+begin
+  Result:=False;
+  FInputs:=GetOptionValues('i','input');
+  if Length(FInputs)=0 then
+    FInputs:=GetNonOptions(ShortOptions,LongOptions);
+  FOutput:=GetOptionValue('o','output');
+  if Length(FInputs)>1 then
+    If not DirectoryExists(FOutput) then
+      begin
+      Usage('Directory does not exist or is not a directory: '+Foutput);
+      exit;
+      end;
+  FFull:=HasOption('f','full');
+  FHTMLTitle:=GetOptionValue('t','title');
+  lFileName:=GetOptionValue('H','head');
+  if (lFilename<>'') then
+    begin
+    if not FileExists(lFileName) then
+      begin
+      Usage('Head matter file does not exist: '+lFileName);
+      exit;
+      end;
+    FHead:=TStringList.Create;
+    FHead.LoadFromFile(lFileName);
+    end;
+  Result:=True;
+end;
+
+constructor TMD2HTMLApplication.Create(aOwner: TComponent);
+begin
+  inherited Create(aOwner);
+  StopOnException:=True;
+end;
+
+destructor TMD2HTMLApplication.Destroy;
+begin
+  FreeAndNil(FHead);
+  inherited Destroy;
+end;
+
+procedure TMD2HTMLApplication.Usage(const ErrMsg : string);
+begin
+  if ErrMsg<>'' then
+    Writeln('Error: ',ErrMsg);
+  Writeln('Usage: ', ExeName, ' [options]');
+  Writeln('Where options is one or more of:');
+  Writeln('-h --help         this message');
+  Writeln('-i --input=FILE   Input markdown file');
+  Writeln('-o --output=FILE  Output HTML file');
+  Writeln('-f --full         Output complete HTML file (html/body/head tags))');
+  Writeln('-H --head=FILE    head tag content file');
+  ExitCode:=Ord(ErrMsg<>'');
+end;
+
+var
+  Application: TMD2HTMLApplication;
+begin
+  Application:=TMD2HTMLApplication.Create(nil);
+  Application.Title:='Markdown to HTML converter';
+  Application.Run;
+  Application.Free;
+end.
+

+ 16 - 0
packages/fcl-md/demo/sampledoc.md

@@ -0,0 +1,16 @@
+# Sample
+
+## Pos
+### Short
+Find position in string
+
+### Descr
+*Pos* returns the position of *Sub* in *S*
+```
+Some code
+```
+### Errors
+None.
+
+### SeeAlso
+[](copy)

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

@@ -0,0 +1,112 @@
+{$ifndef ALLPACKAGES}
+{$mode objfpc}{$H+}
+program fpmake;
+
+uses {$ifdef unix}cthreads,{$endif} fpmkunit;
+
+Var
+  T : TTarget;
+  P : TPackage;
+begin
+  With Installer do
+    begin
+{$endif ALLPACKAGES}
+
+    P:=AddPackage('fcl-md');
+    P.ShortName:='fclmd';
+{$ifdef ALLPACKAGES}
+    P.Directory:=ADirectory;
+{$endif ALLPACKAGES}
+    P.Version:='3.3.1';
+    P.Dependencies.Add('fcl-base');
+    P.Dependencies.Add('rtl-objpas');
+    P.Dependencies.Add('fcl-fpcunit');
+    P.Author := 'Michael van Canneyt';
+    P.License := 'LGPL with modification, ';
+    P.HomepageURL := 'www.freepascal.org';
+    P.Email := '';
+    P.Description := 'Extensible markdown parsing and rendering.';
+    P.NeedLibC:= false;
+    P.OSes:=AllOSes-[embedded,msdos,win16,macosclassic,palmos,zxspectrum,msxdos,amstradcpc,sinclairql,human68k,ps1,wasip2];
+    if Defaults.CPU=jvm then
+      P.OSes := P.OSes - [java,android];
+
+    P.SourcePath.Add('src');
+
+    T:=P.Targets.AddUnit('markdown.utils.pas');
+    T:=P.Targets.AddUnit('markdown.htmlentities.pas');
+
+    T:=P.Targets.AddUnit('markdown.elements.pas');
+    with T.Dependencies do
+      begin
+      AddUnit('markdown.utils');
+     end;
+
+    T:=P.Targets.AddUnit('markdown.line.pas');
+    with T.Dependencies do
+      begin
+      AddUnit('markdown.utils');
+     end;
+
+    T:=P.Targets.AddUnit('markdown.scanner.pas');
+    with T.Dependencies do
+      begin
+      AddUnit('markdown.utils');
+      AddUnit('markdown.elements');
+      end;
+      
+    T:=P.Targets.AddUnit('markdown.inlinetext.pas');
+    with T.Dependencies do
+      begin
+      AddUnit('markdown.utils');
+      AddUnit('markdown.scanner');
+      AddUnit('markdown.elements');
+     end;
+
+    T:=P.Targets.AddUnit('markdown.parser.pas');
+    with T.Dependencies do
+      begin
+      AddUnit('markdown.elements');
+      AddUnit('markdown.utils');
+      AddUnit('markdown.scanner');
+      AddUnit('markdown.line');
+      AddUnit('markdown.inlinetext');
+      AddUnit('markdown.htmlentities');
+      end;
+        
+    T:=P.Targets.AddUnit('markdown.render.pas');
+    with T.Dependencies do
+      begin
+      AddUnit('markdown.elements');
+      AddUnit('markdown.utils');
+      end;
+        
+    T:=P.Targets.AddUnit('markdown.htmlrender.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
+      AddUnit('markdown.elements');
+      AddUnit('markdown.utils');
+      AddUnit('markdown.render');
+      end;
+    
+    P.ExamplePath.Add('demo');
+    T:=P.Targets.AddExampleProgram('demomd.lpr');
+    T:=P.Targets.AddExampleProgram('md2html.lpr');
+    T:=P.Targets.AddExampleProgram('md2fpdoc.lpr');
+
+{$ifndef ALLPACKAGES}
+    Run;
+    end;
+end.
+{$endif ALLPACKAGES}
+
+
+

+ 617 - 0
packages/fcl-md/src/markdown.elements.pas

@@ -0,0 +1,617 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown basic block definitions
+
+    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.Elements;
+
+{$mode ObjFPC}
+{$h+}
+
+interface
+
+uses
+{$IFDEF FPC_DOTTEDUNITS}
+  System.Classes, System.SysUtils, System.Contnrs, System.Regexpr, System.CodePages.unicodedata, 
+{$ELSE}  
+  Classes, SysUtils, Contnrs, RegExpr, UnicodeData, 
+{$ENDIF}
+  MarkDown.Utils, 
+  Markdown.HTMLEntities;
+
+const
+  GrowSize = 10;
+
+Type
+  EMarkDown = class(Exception);
+
+  TWhitespaceMode = (wsLeave, wsTrim, wsStrip);
+
+  TPosition = record
+    line : integer;
+    col : integer;
+  end;
+
+  TMarkDownElement = class (TObject);
+  TMarkDownElementClass = class of TMarkDownElement;
+
+  TTextNodeKind = (nkNamed,nkLineBreak,nkText,nkCode,nkURI,nkEmail,nkImg);
+  TTextNodeKinds = set of TTextNodeKind;
+
+  TNodeStyle = (nsStrong,nsEmph,nsDelete);
+  TNodeStyles = Set of TNodeStyle;
+
+  { TMarkDownTextNode }
+
+  TMarkDownTextNode = class(TMarkDownElement)
+  private
+    FKind: TTextNodeKind;
+    FName : Ansistring;
+    FAttrs: THashTable;
+    FContent : AnsiString;
+    FBuild : RawByteString;
+    FLength : integer;
+    FPos : TPosition;
+    FActive : Boolean;
+    FStyles: TNodeStyles;
+    function GetAttrs: THashTable;
+    function GetHasAttrs: Boolean;
+    function getText : AnsiString;
+    function GetNodetext : ansistring;
+    procedure SetName(const Value: AnsiString);
+    procedure SetActive(const aValue: boolean);
+  public
+    constructor Create(aPos : TPosition; aKind : TTextNodeKind);
+    destructor Destroy; override;
+    procedure AddStyle(aStyle : TNodeStyle);
+    procedure IncCol(aCount : integer);
+    property Kind : TTextNodeKind Read FKind Write FKind;
+    property Name : AnsiString read FName write SetName;
+    property Attrs : THashTable read GetAttrs;
+    property HasAttrs : Boolean Read GetHasAttrs;
+    property NodeText : ansistring read GetNodetext;
+    procedure AddText(ch : char); overload;
+    procedure AddText(s : AnsiString); overload;
+    procedure RemoveChars(count : integer);
+    function IsEmpty : boolean;
+    property Pos : TPosition Read FPos;
+    property Active : Boolean Read FActive Write SetActive;
+    property Styles : TNodeStyles Read FStyles Write FStyles;
+  end;
+
+  { TMarkDownTextNodeList }
+
+  TMarkDownTextNodeList = class (specialize TGFPObjectList<TMarkDownTextNode>)
+  private
+    procedure ClearActive; inline;
+  public
+    // When the last block is active, add to that block. Otherwise, create a new text node
+    function AddText(aPos: TPosition; aContent: AnsiString): TMarkDownTextNode;
+    function AddTextNode(aPos: TPosition; aKind: TTextNodeKind; cnt: AnsiString; aDoClose: Boolean=True): TMarkDownTextNode; // always make a new node, and make it inactive
+    procedure RemoveAfter(node : TMarkDownTextNode);
+    function LastNode : TMarkDownTextNode;
+    procedure ApplyStyleBetween(aStart,aStop : TMarkDownTextNode; aStyle : TNodeStyle);
+  end;
+
+  { TMarkdownBlock }
+
+  TMarkdownBlock = class abstract (TMarkDownElement)
+  private
+    FClosed: boolean;
+    FLine : integer;
+    FParent : TMarkdownBlock;
+  protected
+    procedure SetClosed(const aValue: boolean); virtual;
+    function GetChild(aIndex : Integer): TMarkDownBlock; virtual;
+    function GetChildCount: Integer; virtual;
+    procedure AddChild(aChild : TMarkDownBlock); virtual;
+    function GetLastChild: TMarkDownBlock; virtual;
+  public
+    constructor Create(aParent : TMarkDownBlock; aLine : Integer);  virtual; reintroduce;
+    procedure Dump(const aIndent : string = '');
+    Function GetFirstText : String;
+    function WhitespaceMode : TWhitespaceMode; virtual;
+    property Closed : boolean read FClosed write SetClosed;
+    property Line : Integer read FLine;
+    property Parent : TMarkDownBlock read FParent;
+    property LastChild : TMarkDownBlock Read GetLastChild;
+    property ChildCount : Integer read GetChildCount;
+    property Children[aIndex : Integer] : TMarkDownBlock read GetChild; default;
+  end;
+  TMarkdownBlockClass = class of TMarkdownBlock;
+
+  { TMarkDownBlockList }
+
+  TMarkDownBlockList = class(specialize TGFPObjectList<TMarkdownBlock>)
+    function lastblock : TMarkDownBlock;
+  end;
+
+  { TMarkDownContainerBlock }
+
+  TMarkDownContainerBlock = class (TMarkdownBlock)
+  private
+    FBlocks: TMarkDownBlockList;
+  protected
+    procedure AddChild(aChild : TMarkDownBlock); override;
+    function GetChild(aIndex : Integer): TMarkDownBlock; override;
+    function GetChildCount: Integer; override;
+    function GetLastChild: TMarkDownBlock; override;
+  public
+    constructor Create(aParent : TMarkDownBlock; aLine : Integer); override;
+    destructor Destroy; override;
+    procedure DeleteChild(aIndex : Integer);
+    property Blocks : TMarkDownBlockList read FBlocks;
+  end;
+
+  TMarkDownDocument = class (TMarkDownContainerBlock);
+
+  { TMarkDownParagraphBlock }
+
+  TMarkDownParagraphBlock = class (TMarkDownContainerBlock)
+  private
+    FHeader: integer;
+  public
+    function IsPlainPara : boolean; virtual;
+    property Header : integer read FHeader write FHeader;
+  end;
+
+  TMarkDownQuoteBlock = class (TMarkDownParagraphBlock)
+  public
+    function IsPlainPara : boolean; override;
+  end;
+
+  TMarkDownListBlock = class (TMarkDownContainerBlock)
+  private
+    FOrdered: boolean;
+    FStart: integer;
+    FMarker: AnsiString;
+    FLoose: boolean;
+    FLastIndent: integer;
+    FBaseIndent: integer;
+    FHasSeenEmptyLine : boolean; // parser state
+  public
+    function Grace : integer;
+    property Ordered : boolean read FOrdered write FOrdered;
+    property BaseIndent : integer read FBaseIndent write FBaseIndent;
+    property LastIndent : integer read FLastIndent write FLastIndent;
+    property Start : integer read FStart write FStart;
+    property Marker : AnsiString read FMarker write FMarker;
+    property Loose : boolean read FLoose write FLoose;
+    property HasSeenEmptyLine : boolean read FHasSeenEmptyLine Write FHasSeenEmptyLine; // parser state
+  end;
+
+  TMarkDownListItemBlock = class (TMarkDownParagraphBlock)
+  public
+    function isPlainPara : boolean; override;
+  end;
+
+  TMarkDownHeadingBlock = class (TMarkDownContainerBlock)
+  private
+    FLevel: integer;
+  public
+    constructor Create(aParent : TMarkDownBlock; aLine, alevel : Integer); reintroduce;
+    property Level : integer read FLevel write FLevel;
+  end;
+
+  TMarkDownCodeBlock = class (TMarkDownContainerBlock)
+  private
+    FFenced: boolean;
+    FLang: AnsiString;
+  public
+    function WhiteSpaceMode : TWhitespaceMode; override;
+    property Fenced : boolean read FFenced write FFenced;
+    property Lang : AnsiString read FLang write FLang;
+  end;
+
+
+  TMarkDownTableRowBlock = class (TMarkDownContainerBlock);
+
+  TCellAlign = (caLeft, caCenter, caRight);
+  TCellAlignArray = array of TCellAlign;
+
+  { TMarkDownTableBlock }
+
+  TMarkDownTableBlock = class (TMarkDownContainerBlock)
+  private
+    FColumns: TCellAlignArray;
+  public
+    property Columns : TCellAlignArray read FColumns Write FColumns;
+  end;
+
+  TMarkDownLeafBlock = class abstract (TMarkdownBlock);
+
+  TMarkDownThematicBreakBlock = class (TMarkDownLeafBlock)
+  end;
+
+  { TMarkDownTextBlock }
+
+  TMarkDownTextBlock = class (TMarkDownLeafBlock)
+  private
+    FText: AnsiString;
+    FNodes : TMarkDownTextNodeList;
+  protected
+    procedure SetClosed(const aValue: boolean);override;
+  public
+    constructor Create(aParent : TMarkDownBlock; aLine : integer; aText : AnsiString); reintroduce;
+    destructor Destroy; override;
+    property Text : AnsiString read FText write FText;
+    property Nodes : TMarkDownTextNodeList Read FNodes Write FNodes;
+  end;
+
+implementation
+
+{ TMarkDownTextNode }
+
+procedure TMarkDownTextNode.addText(s: AnsiString);
+
+var
+  len,AddLen,NewLen : Integer;
+begin
+  AddLen:=Length(S);
+  if AddLen=0 then exit;
+  Len:=Length(FBuild);
+  NewLen:=FLength+AddLen;
+  if NewLen>=Len then
+    setLength(FBuild, NewLen+GrowSize);
+  Move(S[1],FBuild[FLength+1],AddLen);
+  inc(FLength,AddLen);
+end;
+
+constructor TMarkDownTextNode.Create(aPos : TPosition; aKind : TTextNodeKind);
+begin
+  inherited Create;
+  FActive:=True;
+  FPos:=aPos;
+  FKind:=aKind;
+end;
+
+procedure TMarkDownTextNode.addText(ch: char);
+var
+  len : Integer;
+begin
+  Len:=Length(FBuild);
+  if FLength>=Len then
+    setLength(FBuild, Len+GrowSize);
+  inc(FLength);
+  FBuild[FLength]:=ch;
+end;
+
+destructor TMarkDownTextNode.Destroy;
+begin
+  FreeAndNil(FAttrs);
+  inherited;
+end;
+
+procedure TMarkDownTextNode.AddStyle(aStyle: TNodeStyle);
+begin
+  include(FStyles,aStyle);
+end;
+
+procedure TMarkDownTextNode.IncCol(aCount: integer);
+begin
+  Inc(FPos.Col,aCount);
+end;
+
+function TMarkDownTextNode.GetAttrs: THashTable;
+begin
+  if FAttrs = nil then
+    FAttrs:=THashTable.create;
+  Result:=FAttrs;
+end;
+
+function TMarkDownTextNode.GetHasAttrs: Boolean;
+begin
+  Result:=Assigned(FAttrs);
+end;
+
+function TMarkDownTextNode.getText: AnsiString;
+begin
+  Active:=False;
+  Result:=FContent;
+end;
+
+function TMarkDownTextNode.GetNodetext: ansistring;
+begin
+  Result:=Copy(FContent,1,Length(FContent));
+end;
+
+function TMarkDownTextNode.isEmpty: boolean;
+begin
+  Result:=FContent = '';
+end;
+
+procedure TMarkDownTextNode.removeChars(count : integer);
+begin
+  Active:=False;
+  delete(FContent, 1, count);
+end;
+
+
+procedure TMarkDownTextNode.SetActive(const aValue: boolean);
+begin
+  if FActive and not aValue then
+    FContent:=Copy(FBuild,1,FLength);
+  FActive:=aValue;
+end;
+
+procedure TMarkDownTextNode.SetName(const Value: AnsiString);
+begin
+  FName:=Value;
+  Active:=False;
+end;
+
+{ TMarkDownTextNodeList }
+
+procedure TMarkDownTextNodeList.ClearActive;
+begin
+  if count > 0 then
+    Self[Count-1].Active:=False;
+end;
+
+procedure TMarkDownTextNodeList.ApplyStyleBetween(aStart,aStop : TMarkDownTextNode; aStyle : TNodeStyle);
+
+var
+  Idx,lStart,lStop : Integer;
+
+begin
+  lStart:=IndexOf(aStart);
+  lStop:=IndexOf(aStop)-1;
+  For Idx:=lStart to lStop do
+    begin
+    Elements[Idx].AddStyle(aStyle)
+    end;
+end;
+
+function TMarkDownTextNodeList.addText(aPos : TPosition; aContent: AnsiString): TMarkDownTextNode;
+var
+  lNode : TMarkDownTextNode;
+begin
+  lNode:=Nil;
+  if (Count>0) then
+    lNode:=Elements[Count-1];
+  if (lNode=Nil) or not lNode.Active then
+    begin
+    Result:=TMarkDownTextNode.Create(aPos,nkText);
+    add(Result);
+    end
+  else
+    Result:=lNode;
+  Result.addText(aContent);
+end;
+
+function TMarkDownTextNodeList.addTextNode(aPos : TPosition; aKind : TTextNodeKind; cnt: AnsiString; aDoClose : Boolean = True): TMarkDownTextNode;
+begin
+  ClearActive;
+  Result:=TMarkDownTextNode.Create(aPos,aKind);
+  add(Result);
+  Result.addText(cnt);
+  if aDoClose then
+    Result.Active:=False;
+end;
+
+procedure TMarkDownTextNodeList.removeAfter(node: TMarkDownTextNode);
+var
+  i, idx : integer;
+
+begin
+  idx:=indexOf(node);
+  for i:=count-1 downto idx+1 do
+    Delete(i);
+end;
+
+function TMarkDownTextNodeList.lastNode: TMarkDownTextNode;
+begin
+  Result:=Nil;
+  if Count>0 then
+    Result:=Elements[Count-1];
+end;
+
+{ TMarkdownBlock }
+
+function TMarkdownBlock.GetLastChild: TMarkDownBlock;
+begin
+  Result:=Nil;
+end;
+
+procedure TMarkdownBlock.SetClosed(const aValue: boolean);
+begin
+  if FClosed=aValue then Exit;
+  FClosed:=aValue;
+  if aValue and (ChildCount>0) then
+    Children[ChildCount-1].Closed:=True;
+end;
+
+function TMarkdownBlock.GetChild(aIndex : Integer): TMarkDownBlock;
+begin
+  if aIndex<0 then ; // Silence compiler warning
+  Result:=Nil;
+end;
+
+function TMarkdownBlock.GetChildCount: Integer;
+begin
+  Result:=0;
+end;
+
+procedure TMarkdownBlock.AddChild(aChild: TMarkDownBlock);
+begin
+  if (aChild<>Nil) then
+  Raise Exception.Create('Cannot add child to simple block');
+end;
+
+constructor TMarkdownBlock.Create(aParent: TMarkDownBlock; aLine: Integer);
+begin
+  Inherited Create;
+  FParent:=aParent;
+  if assigned(aParent) then
+    aParent.AddChild(Self);
+  FLine:=aLine;
+end;
+
+procedure TMarkdownBlock.dump(const aIndent: string = '');
+var
+  I : Integer;
+begin
+  Write(aIndent);
+  if not Closed then
+    Write('! ')
+  else
+    Write('  ');
+  Writeln(ClassName);
+  For I:=0 to ChildCount-1 do
+    Children[i].Dump(aIndent+'  ');
+end;
+
+function TMarkdownBlock.WhiteSpaceMode: TWhitespaceMode;
+begin
+  Result:=wsTrim;
+end;
+
+function TMarkDownBlock.GetFirstText: String;
+var
+  lText : TMarkDownTextBlock;
+begin
+  Result:='';
+  if ChildCount=0 then
+    exit;
+  if not (Children[0] is TMarkDownTextBlock) then
+    exit;
+  lText:=TMarkDownTextBlock(Children[0]);
+  if lText.Nodes.Count=0 then
+    exit;
+  Result:=lText.Nodes[0].NodeText;
+end;
+
+{ TMarkDownBlockList }
+
+function TMarkDownBlockList.lastblock: TMarkDownBlock;
+begin
+  Result:=nil;
+  if Count>0 then
+    Result:=Self[Count-1];
+end;
+
+{ TMarkDownContainerBlock }
+
+procedure TMarkDownContainerBlock.AddChild(aChild: TMarkDownBlock);
+begin
+  if aChild=Nil then
+    Raise EMarkDown.CreateFmt('Cannot add nil child to block "%s"',[ClassName]);
+  FBlocks.Add(aChild);
+end;
+
+function TMarkDownContainerBlock.GetChild(aIndex: Integer): TMarkDownBlock;
+begin
+  Result:=FBlocks[aIndex];
+end;
+
+function TMarkDownContainerBlock.GetChildCount: Integer;
+begin
+  Result:=FBlocks.Count;
+end;
+
+function TMarkDownContainerBlock.GetLastChild: TMarkDownBlock;
+begin
+  if FBlocks.Count>0 then
+    Result:=FBlocks[FBlocks.Count-1]
+  else
+    Result:=Nil;
+end;
+
+constructor TMarkDownContainerBlock.Create(aParent : TMarkDownBlock; aLine : Integer);
+begin
+  inherited create(aParent,aLine);
+  FBlocks:=TMarkDownBlockList.Create(true);
+end;
+
+destructor TMarkDownContainerBlock.Destroy;
+begin
+  FreeAndNil(FBlocks);
+  inherited Destroy;
+end;
+
+procedure TMarkDownContainerBlock.DeleteChild(aIndex: Integer);
+begin
+  FBlocks.Delete(aIndex);
+end;
+
+{ TMarkDownParagraphBlock }
+
+function TMarkDownParagraphBlock.IsPlainPara: boolean;
+begin
+  Result:=FHeader = 0;
+end;
+
+
+{ TMarkDownQuoteBlock }
+
+function TMarkDownQuoteBlock.isPlainPara: boolean;
+begin
+  Result:=false;
+end;
+
+{ TMarkDownListBlock }
+
+function TMarkDownListBlock.grace: integer;
+begin
+  if ordered then
+    Result:=2
+  else
+    Result:=1;
+end;
+
+{ TMarkDownListItemBlock }
+
+function TMarkDownListItemBlock.isPlainPara: boolean;
+begin
+  Result:=false;
+end;
+
+{ TMarkDownHeadingBlock }
+
+constructor TMarkDownHeadingBlock.Create(aParent : TMarkDownBlock;aLine, aLevel: Integer);
+begin
+  Inherited Create(aParent,aLine);
+  FLevel:=aLevel;
+end;
+
+{ TMarkDownCodeBlock }
+
+
+function TMarkDownCodeBlock.WhiteSpaceMode: TWhitespaceMode;
+begin
+  Result:=wsLeave;
+end;
+
+{ TMarkDownTableBlock }
+
+{ TMarkDownTextBlock }
+
+procedure TMarkDownTextBlock.SetClosed(const aValue: boolean);
+begin
+  inherited SetClosed(aValue);
+  if assigned(FNodes) then
+    FNodes.ClearActive;
+end;
+
+constructor TMarkDownTextBlock.Create(aParent: TMarkDownBlock; aLine: integer; aText: AnsiString);
+begin
+  inherited Create(aParent,aLine);
+  FText:=aText;
+end;
+
+destructor TMarkDownTextBlock.Destroy;
+begin
+  FreeAndNil(FNodes);
+  inherited;
+end;
+
+end.

+ 917 - 0
packages/fcl-md/src/markdown.fpdocrender.pas

@@ -0,0 +1,917 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown FPDoc input file 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.FPDocRender;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+{$IFDEF FPC_DOTTEDUNITS}
+  System.Classes, System.SysUtils, System.StrUtils, System.Contnrs, Xml.Dom, Xml.Writer,
+{$ELSE}  
+  Classes, SysUtils, strutils, contnrs, dom, XMLWrite,
+{$ENDIF}
+  MarkDown.Elements, MarkDown.Render, MarkDown.Utils;
+
+type
+  TElementType = (etPackage, etModule, etTopic, etElement);
+  TSectionType = (stShort,stDescription,stErrors,stExamples,stSeeAlso);
+
+const
+  SectionNodeNames : Array[TSectionType] of string = ('short','descr','errors','examples','seealso');
+  ElementNodeNames : Array[TElementType] of string = ('package','module','topic','element');
+
+type
+  EFPDocRender = Class(EMarkDown);
+  { TMarkDownFPDocRenderer }
+
+  TMarkDownFPDocRenderer = class(TMarkDownRenderer)
+  private
+    FDoc : TXMLDocument;
+    FFPDoc: String;
+    FPackageName: String;
+    FStack: Array[0..100] of TDomElement;
+    FStackCount : Integer;
+    FSkipParagraph : Boolean;
+    function GetParent: TDomElement;
+    procedure PushElement(aElement : TDomElement);
+    function PopElement : TDomElement;
+  Protected
+    Procedure AppendText(const aContent : String);
+    function Push(const aElementName : String; const aName : string = '') : TDOMElement;
+    function PushSection(aSection : TSectionType) : TDomElement;
+    function Pop : TDomElement;
+    function PopTill(const aElementName : string) : TDomElement;
+    function PopTill(const aElementNames : array of string) : TDomElement;
+    Property Doc : TXMLDocument Read FDoc;
+    Property Parent : TDomElement Read GetParent;
+    Property SkipParagraph : Boolean Read FSkipParagraph;
+  public
+    procedure RenderToXML(aDocument : TMarkDownDocument; aXML : TXMLDocument);
+    procedure RenderToStream(aDocument : TMarkDownDocument; aStream : TStream);
+    Procedure RenderDocument(aDocument : TMarkDownDocument); override;overload;
+    Procedure RenderDocument(aDocument : TMarkDownDocument; aDest : TStrings); overload;
+    procedure RenderChildren(aBlock : TMarkDownContainerBlock; aAppendNewLine : Boolean); overload;
+    function RenderFPDoc(aDocument : TMarkDownDocument) : string;
+    Property PackageName : String read FPackageName Write FPackageName;
+    Property FPDoc : String Read FFPDoc;
+  end;
+
+  { TFPDocMarkDownBlockRenderer }
+
+  TFPDocMarkDownBlockRenderer = Class (TMarkDownBlockRenderer)
+  Private
+    function GetFPDocRenderer: TMarkDownFPDocRenderer;
+    function GetParent: TDomElement;
+  protected
+    procedure CheckParent(const aParent,aChild : String);
+    function CheckIsValidName(const aText : string) : boolean;
+    function GetSectionType(const aText: string): TSectionType;
+    function GetElementType(var aText: string): TElementType;
+    procedure Append(const S : String); inline;
+    procedure AppendNl(const S : String = ''); inline;
+  public
+    property FPDoc : TMarkDownFPDocRenderer Read GetFPDocRenderer;
+    Property Parent : TDomElement Read GetParent;
+  end;
+  TFPDocMarkDownBlockRendererClass = class of TFPDocMarkDownBlockRenderer;
+  { TFPDocMarkDownTextRenderer }
+
+  TFPDocMarkDownTextRenderer = class(TMarkDownTextRenderer)
+  Private
+    FStyleStack: Array of TNodeStyle;
+    FStyleStackLen : Integer;
+    FLastStyles : TNodeStyles;
+    FKeys : Array of String;
+    FKeyCount : integer;
+    FText : String;
+    procedure DoKey(aItem: AnsiString; const aKey: AnsiString; var aContinue: Boolean);
+    procedure EmitStyleDiff(aStyles: TNodeStyles);
+    function GetFPDocRenderer: TMarkDownFPDocRenderer;
+    function GetNodeTag(aElement: TMarkDownTextNode): string;
+    function MustCloseNode(aElement: TMarkDownTextNode): boolean;
+  protected
+    procedure StartText;
+    procedure EndText;
+    procedure PushStyle(aStyle : TNodeStyle);
+    function PopStyles(aStyle: TNodeStyles): TNodeStyle;
+    procedure PopStyle(aStyle : TNodeStyle);
+    procedure Append(const S : String); inline;
+    procedure DoRender(aElement: TMarkDownTextNode); override;
+  Public
+    procedure BeginBlock; override;
+    procedure EndBlock; override;
+    property FPDoc : TMarkDownFPDocRenderer Read GetFPDocRenderer;
+    function renderAttrs(aElement: TMarkDownTextNode): AnsiString;
+  end;
+  TFPDocMarkDownTextRendererClass = class of TFPDocMarkDownTextRenderer;
+
+  { TFPDocParagraphBlockRenderer }
+
+  TFPDocParagraphBlockRenderer = class (TFPDocMarkDownBlockRenderer)
+  protected
+    procedure DoRender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TFPDocMarkDownQuoteBlockRenderer }
+
+  TFPDocMarkDownQuoteBlockRenderer = class(TFPDocMarkDownBlockRenderer)
+  protected
+    procedure dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TFPDocMarkDownTextBlockRenderer }
+
+  TFPDocMarkDownTextBlockRenderer = class(TFPDocMarkDownBlockRenderer)
+  protected
+    procedure DoRender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TFPDocMarkDownListBlockRenderer }
+
+  TFPDocMarkDownListBlockRenderer = class(TFPDocMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TFPDocMarkDownListItemBlockRenderer }
+
+  TFPDocMarkDownListItemBlockRenderer = class(TFPDocMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TFPDocMarkDownCodeBlockRenderer }
+
+  TFPDocMarkDownCodeBlockRenderer = class(TFPDocMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TFPDocMarkDownHeadingBlockRenderer }
+
+  TFPDocMarkDownHeadingBlockRenderer = class(TFPDocMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TFPDocMarkDownThematicBreakBlockRenderer }
+
+  TFPDocMarkDownThematicBreakBlockRenderer = class(TFPDocMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TFPDocMarkDownTableBlockRenderer }
+
+  TFPDocMarkDownTableBlockRenderer = class(TFPDocMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TMarkDownTableRowBlockRenderer }
+
+  TFPDocMarkDownTableRowBlockRenderer = class(TFPDocMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TFPDocMarkDownDocumentRenderer }
+
+  TFPDocMarkDownDocumentRenderer = class(TFPDocMarkDownBlockRenderer)
+  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;
+
+{ TMarkDownBlockRenderer }
+
+function TFPDocMarkDownBlockRenderer.GetFPDocRenderer: TMarkDownFPDocRenderer;
+begin
+  if Renderer is TMarkDownFPDocRenderer then
+    Result:=TMarkDownFPDocRenderer(Renderer)
+  else
+    Result:=Nil;
+end;
+
+function TFPDocMarkDownBlockRenderer.GetParent: TDomElement;
+begin
+  Result:=FPDoc.Parent;
+end;
+
+procedure TFPDocMarkDownBlockRenderer.CheckParent(const aParent, aChild: String);
+begin
+  if (Parent.NodeName<>aParent) then
+    Raise EFPDocRender.CreateFmt('Cannot have %s below %s',[aChild,aParent]);
+end;
+
+function TFPDocMarkDownBlockRenderer.CheckIsValidName(const aText: string): boolean;
+const
+  StartIdentChars = ['a'..'z','A'..'Z','_'];
+  AllIdentChars = StartIdentChars+['.','_','0'..'9'];
+var
+  I,Len : integer;
+begin
+  len:=Length(aText);
+  Result:=(Len>0) and (aText[1] in StartIdentChars);
+  I:=2;
+  While Result and (I<=Len) do
+    begin
+    Result:=aText[I] in AllIdentChars;
+    inc(i);
+    end;
+end;
+
+function TFPDocMarkDownBlockRenderer.GetSectionType(const aText: string): TSectionType;
+var
+  lText : string;
+begin
+  lText:=LowerCase(Trim(aText));
+  case lText of
+    'short' : result:=stShort;
+    'descr',
+    'description' : result:=stDescription;
+    'errors' : Result:=stErrors;
+    'example',
+    'examples' : Result:=stExamples;
+    'seealso': Result:=stSeeAlso;
+  else
+    result:=stDescription;
+  end;
+end;
+
+function TFPDocMarkDownBlockRenderer.GetElementType(var aText: string): TElementType;
+var
+  p : integer;
+begin
+  Result:=etElement;
+  aText:=Trim(aText);
+  p:=Pos(':',aText);
+  if p=0 then
+    exit;
+  if SameText(Copy(aText,1,P-1),'topic') then
+    begin
+    Result:=etTopic;
+    Delete(aText,1,P);
+    aText:=Trim(aText);
+    end;
+end;
+
+procedure TFPDocMarkDownBlockRenderer.Append(const S: String);
+begin
+  FPDoc.AppendText(S);
+end;
+
+procedure TFPDocMarkDownBlockRenderer.AppendNl(const S: String);
+begin
+  FPDoc.AppendText(S);
+end;
+
+
+
+{ TMarkDownFPDocRenderer }
+
+function TMarkDownFPDocRenderer.GetParent: TDomElement;
+begin
+  if FStackCount>0 then
+    Result:=FStack[FStackCount-1]
+  else
+    Result:=Nil;
+end;
+
+function TMarkDownFPDocRenderer.Push(const aElementName: String; const aName: string): TDOMElement;
+begin
+  Result:=FDoc.CreateElement(aElementName);
+  PushElement(Result);
+  if aName<>'' then
+    Result['name']:=aName;
+end;
+
+procedure TMarkDownFPDocRenderer.PushElement(aElement: TDomElement);
+begin
+  if FStackCount=Length(FStack) then
+    Raise EFPDocRender.Create('Max stack size reached');
+  if FStackCount=0 then
+    FDoc.AppendChild(aElement)
+  else
+    Parent.AppendChild(aElement);
+  FStack[FStackCount]:=aElement;
+  Inc(FStackCount);
+end;
+
+function TMarkDownFPDocRenderer.PopElement: TDomElement;
+begin
+  if FStackCount>0 then
+    begin
+    Result:=FStack[FStackCount-1];
+    Dec(FStackCount);
+    end
+  else
+    Result:=Nil;
+end;
+
+procedure TMarkDownFPDocRenderer.AppendText(const aContent: String);
+begin
+  Parent.AppendChild(FDoc.CreateTextNode(aContent))
+end;
+
+function TMarkDownFPDocRenderer.PushSection(aSection: TSectionType): TDomElement;
+begin
+  Result:=Push(SectionNodeNames[aSection]);
+  FSkipParagraph:=aSection in [stShort,stSeeAlso];
+end;
+
+function TMarkDownFPDocRenderer.Pop: TDomElement;
+begin
+  Result:=PopElement;
+end;
+
+function TMarkDownFPDocRenderer.PopTill(const aElementName: string): TDomElement;
+begin
+  PopTill([aElementName]);
+end;
+
+
+function TMarkDownFPDocRenderer.PopTill(const aElementNames: array of string): TDomElement;
+begin
+  FSkipParagraph:=False;
+  While IndexStr(UTF8Encode(Parent.NodeName),aElementNames)=-1 do
+    begin
+    Pop;
+    if Parent=Nil then
+      Raise EFPDocRender.CreateFmt('Could not pop to %s',[aElementNames[0]]);
+    end;
+  Result:=Parent;
+end;
+
+procedure TMarkDownFPDocRenderer.RenderToXML(aDocument: TMarkDownDocument; aXML: TXMLDocument);
+begin
+  FDoc:=aXML;
+  try
+    Push('fpdoc-descriptions');
+    Push('package',FPackageName);
+    RenderBlock(aDocument);
+    Pop;
+    Pop;
+  finally
+    FDoc:=Nil;
+  end;
+end;
+
+
+procedure TMarkDownFPDocRenderer.RenderToStream(aDocument: TMarkDownDocument; aStream: TStream);
+var
+  lDoc : TXMLDocument;
+begin
+  LDoc:=TXMLDocument.Create;
+  try
+    RenderToXML(aDocument,LDoc);
+    WriteXML(LDoc,aStream);
+  finally
+    FreeAndNil(LDoc);
+  end;
+end;
+
+procedure TMarkDownFPDocRenderer.RenderDocument(aDocument: TMarkDownDocument);
+var
+  S : TStringStream;
+begin
+  S:=TStringStream.Create('');
+  try
+    RenderToStream(aDocument,S);
+    FFPDoc:=S.DataString;
+  finally
+    S.Free;
+  end;
+end;
+
+procedure TMarkDownFPDocRenderer.RenderDocument(aDocument: TMarkDownDocument; aDest: TStrings);
+begin
+  aDest.Text:=RenderFPDoc(aDocument);
+end;
+
+procedure TMarkDownFPDocRenderer.RenderChildren(aBlock: TMarkDownContainerBlock; aAppendNewLine: Boolean);
+var
+  i : integer;
+begin
+  for I:=0 to aBlock.Blocks.Count-1 do
+    RenderBlock(aBlock.Blocks[I]);
+end;
+
+function TMarkDownFPDocRenderer.RenderFPDoc(aDocument: TMarkDownDocument): string;
+begin
+  RenderDocument(aDocument);
+  Result:=FFPDoc;
+  FFPDoc:='';
+end;
+
+
+procedure TFPDocMarkDownTextRenderer.Append(const S: String);
+begin
+  FText:=FText+S;
+end;
+
+function TFPDocMarkDownTextRenderer.MustCloseNode(aElement: TMarkDownTextNode) : boolean;
+
+begin
+  Result:=aElement.kind<>nkImg;
+end;
+
+procedure TFPDocMarkDownTextRenderer.StartText;
+begin
+  FText:='';
+end;
+
+procedure TFPDocMarkDownTextRenderer.EndText;
+begin
+  FPDoc.AppendText(FText);
+  FText:='';
+end;
+
+const
+  StyleNames : Array[TNodeStyle] of string = ('b','i','u');
+
+procedure TFPDocMarkDownTextRenderer.PushStyle(aStyle: TNodeStyle);
+
+begin
+  FPDoc.Push(styleNames[aStyle]);
+  if FStyleStackLen=Length(FStyleStack) then
+    SetLength(FStyleStack,FStyleStackLen+3);
+  FStyleStack[FStyleStackLen]:=aStyle;
+  Inc(FStyleStackLen);
+end;
+
+function TFPDocMarkDownTextRenderer.PopStyles(aStyle: TNodeStyles): TNodeStyle;
+
+begin
+  if (FStyleStackLen>0) and (FStyleStack[FStyleStackLen-1] in aStyle) then
+    begin
+    Result:=FStyleStack[FStyleStackLen-1];
+    FPDoc.Pop;
+    Dec(FStyleStackLen);
+    end;
+end;
+
+procedure TFPDocMarkDownTextRenderer.PopStyle(aStyle: TNodeStyle);
+begin
+  if (FStyleStackLen>0) and (FStyleStack[FStyleStackLen-1]=aStyle) then
+    begin
+    FPDoc.Pop;
+    Dec(FStyleStackLen);
+    end;
+end;
+
+function TFPDocMarkDownTextRenderer.GetNodeTag(aElement: TMarkDownTextNode) : string;
+begin
+  case aElement.Kind of
+    nkCode: Result:='code';
+    nkImg : Result:='img';
+    nkURI,nkEmail : Result:='link'
+  end;
+end;
+
+function TFPDocMarkDownTextRenderer.GetFPDocRenderer: TMarkDownFPDocRenderer;
+begin
+  if Renderer is TMarkDownFPDocRenderer then
+    Result:=TMarkDownFPDocRenderer(Renderer)
+  else
+    Result:=Nil;
+end;
+
+procedure TFPDocMarkDownTextRenderer.DoKey(aItem: AnsiString; const aKey: Ansistring; var aContinue: Boolean);
+begin
+  aContinue:=True;
+  FKeys[FKeyCount]:=aKey;
+  inc(FKeyCount);
+end;
+
+procedure TFPDocMarkDownTextRenderer.EmitStyleDiff(aStyles : TNodeStyles);
+
+var
+  lRemove : TNodeStyles;
+  lAdd : TNodeStyles;
+  S : TNodeStyle;
+
+begin
+  lRemove:=[];
+  lAdd:=[];
+  For S in TNodeStyle do
+    begin
+    if (S in FLastStyles) and Not (S in aStyles) then
+      Include(lRemove,S);
+    if (S in aStyles) and Not (S in FLastStyles) then
+      Include(lAdd,S);
+    end;
+  While lRemove<>[] do
+    begin
+    S:=PopStyles(lRemove);
+    Exclude(lRemove,S);
+    end;
+  For S in TNodeStyle do
+    if S in lAdd then
+      PushStyle(S);
+  FLastStyles:=aStyles;
+end;
+
+procedure TFPDocMarkDownTextRenderer.DoRender(aElement: TMarkDownTextNode);
+
+begin
+  EmitStyleDiff(aElement.Styles);
+  if aElement.Kind<>nkText then
+    begin
+    FPDoc.Push(GetNodeTag(aElement));
+    RenderAttrs(aElement);
+    end;
+  StartText;
+  if aElement.NodeText<>'' then
+    Append(aElement.NodeText);
+  EndText;
+  if aElement.Kind<>nkText then
+    FPDoc.Pop;
+  aElement.Active:=False;
+end;
+
+procedure TFPDocMarkDownTextRenderer.BeginBlock;
+begin
+  inherited BeginBlock;
+  FStyleStackLen:=0;
+  FLastStyles:=[];
+end;
+
+procedure TFPDocMarkDownTextRenderer.EndBlock;
+begin
+  While (FStyleStackLen>0) do
+    Popstyle(FStyleStack[FStyleStackLen-1]);
+  FLastStyles:=[];
+  inherited EndBlock;
+end;
+
+function TFPDocMarkDownTextRenderer.renderAttrs(aElement: TMarkDownTextNode): AnsiString;
+
+  function KeyAlias(const aKey : string): string;
+
+  begin
+    case aKey of
+      'src' : Result:='file';
+      'href' : Result:='id';
+      'alt' : Result:='title';
+    else
+      Result:='';
+    end
+  end;
+
+  procedure addKey(const aKey,aValue : String);
+  var
+    lKey : String;
+  begin
+    lKey:=KeyAlias(aKey);
+    if lKey<>'' then
+      FPDoc.Parent[lKey]:=aValue;
+  end;
+
+var
+  lKey,lAttr : String;
+  lAttrs : THashTable;
+  lKeys : Array of string;
+begin
+  result := '';
+  if not Assigned(aElement.Attrs) then
+    exit;
+  lAttrs:=aElement.Attrs;
+  // First the known keys
+  lKeys:=['src','alt','href','title'];
+  for lKey in lKeys do
+    if lAttrs.TryGet(lKey,lAttr) then
+      AddKey(lKey,lAttr);
+  // Then the other keys
+  SetLength(FKeys,lAttrs.Count);
+  FKeyCount:=0;
+  lAttrs.Iterate(@DoKey);
+  for lKey in FKeys do
+    if IndexStr(lKey,['src','alt','href','title'])=-1 then
+      AddKey(lKey,lAttrs[lKey]);
+end;
+
+procedure TFPDocParagraphBlockRenderer.DoRender(aElement: TMarkDownBlock);
+var
+  lNode : TMarkDownParagraphBlock absolute aElement;
+  et : TElementType;
+  st : TSectionType;
+  lText : string;
+begin
+  if lNode.header=0 then
+    begin
+    if not FPDoc.SkipParagraph then
+      FPDoc.Push('p');
+    end
+  else
+    begin
+    lText:=Trim(lNode.GetFirstText);
+    Case lNode.header of
+    1:
+      begin
+      fpDoc.PopTill('package');
+      CheckIsValidName(lText);
+      FPDoc.Push(ElementNodeNames[etModule],lText);
+      end;
+    2:
+      begin
+      et:=GetElementType(lText);
+      CheckIsValidName(lText);
+      fpDoc.PopTill('module');
+      FPDoc.Push(ElementNodeNames[et]);
+      end;
+    3:
+      begin
+      st:=GetSectionType(lText);
+      FPDoc.PushSection(st);
+      end;
+    end;
+    end;
+  Renderer.RenderChildren(lNode);
+end;
+
+class function TFPDocParagraphBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownParagraphBlock;
+end;
+
+class function TFPDocMarkDownTextBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownTextBlock;
+end;
+
+procedure TFPDocMarkDownTextBlockRenderer.DoRender(aElement: TMarkDownBlock);
+var
+  lNode : TMarkDownTextBlock absolute aElement;
+begin
+  if assigned(lNode) and assigned(lNode.Nodes) then
+    Renderer.RenderTextNodes(lNode.Nodes);
+end;
+
+procedure TFPDocMarkDownQuoteBlockRenderer.dorender(aElement: TMarkDownBlock);
+var
+  lNode : TMarkdownQuoteBlock absolute aElement;
+
+begin
+  CheckParent('descr','remark');
+  fpDoc.Push('remark');
+  Renderer.RenderChildren(lNode);
+  fpDoc.Pop;
+end;
+
+class function TFPDocMarkDownQuoteBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownQuoteBlock;
+end;
+
+procedure TFPDocMarkDownListBlockRenderer.Dorender(aElement : TMarkDownBlock);
+
+var
+  lNode : TMarkDownListBlock absolute aElement;
+  lNodeKind : String;
+begin
+  if not lNode.Ordered then
+    lNodeKind:='ul'
+  else
+    lNodeKind:='ol';
+  FPDoc.Push(lNodeKind);
+  Renderer.RenderChildren(lNode);
+  FPDoc.Pop;
+end;
+
+class function TFPDocMarkDownListBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownListBlock;
+end;
+
+
+procedure TFPDocMarkDownListItemBlockRenderer.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
+  fpDoc.Push('li');
+  For lBlock in lItemBlock.Blocks do
+    if IsPlainBlock(lBlock) then
+      FPDoc.RenderChildren(lPar,True)
+    else
+      Renderer.RenderBlock(lBlock);
+  fpDoc.Pop;
+end;
+
+class function TFPDocMarkDownListItemBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownListItemBlock;
+end;
+
+procedure TFPDocMarkDownCodeBlockRenderer.Dorender(aElement : TMarkDownBlock);
+var
+  lNode : TMarkDownCodeBlock absolute aElement;
+  lBlock : TMarkDownBlock;
+
+begin
+  FPDoc.Push('code');
+  for lBlock in LNode.Blocks do
+    begin
+    Renderer.RenderBlock(LBlock);
+    AppendNl;
+    end;
+  FPDoc.Pop;
+end;
+
+class function TFPDocMarkDownCodeBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownCodeBlock;
+end;
+
+procedure TFPDocMarkDownThematicBreakBlockRenderer.Dorender(aElement : TMarkDownBlock);
+
+begin
+  if Not Assigned(aElement) then;
+end;
+
+class function TFPDocMarkDownThematicBreakBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownThematicBreakBlock;
+end;
+
+{ TMarkDownTableBlock }
+
+procedure TFPDocMarkDownTableBlockRenderer.Dorender(aElement: TMarkDownBlock);
+var
+  lNode : TMarkDownTableBlock absolute aElement;
+  i : integer;
+begin
+  fpdoc.Push('table');
+  Renderer.RenderBlock(lNode.blocks[0]);
+  if lNode.blocks.Count > 1 then
+  begin
+    for i := 1 to lNode.blocks.Count -1  do
+      Renderer.RenderBlock(lnode.blocks[i]);
+  end;
+  fpDoc.Pop;
+end;
+
+class function TFPDocMarkDownTableBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownTableBlock;
+end;
+
+{ TFPDocMarkDownDocumentRenderer }
+
+procedure TFPDocMarkDownDocumentRenderer.Dorender(aElement: TMarkDownBlock);
+
+begin
+  Renderer.RenderChildren(aElement as TMarkDownDocument);
+end;
+
+class function TFPDocMarkDownDocumentRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownDocument
+end;
+
+{ TMarkDownTableRowBlock }
+
+procedure TFPDocMarkDownTableRowBlockRenderer.Dorender(aElement : TMarkDownBlock);
+const
+  CellTypes : Array[Boolean] of string = ('td','th'); //
+var
+  lNode : TMarkDownTableRowBlock absolute aElement;
+  first : boolean;
+  i : integer;
+  cType : String;
+begin
+  first:=(lNode.parent as TMarkDownContainerBlock).blocks.First = self;
+  cType:=Celltypes[First];
+  fpDoc.Push('tr');
+  for i := 0 to length((lNode.parent as TMarkDownTableBlock).Columns) - 1 do
+    begin
+    fpDoc.Push(cType);
+    if i < lNode.blocks.Count then
+      Renderer.RenderBlock(lNode.blocks[i]);
+    fpDoc.Pop;
+    end;
+  fpDoc.Pop;
+end;
+
+class function TFPDocMarkDownTableRowBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownTableRowBlock;
+end;
+
+procedure TFPDocMarkDownHeadingBlockRenderer.Dorender(aElement : TMarkDownBlock);
+var
+  lNode : TMarkDownHeadingBlock absolute aElement;
+  lText : String;
+  et : TElementType;
+  st : TSectionType;
+begin
+  lText:=Trim(lNode.GetFirstText);
+  Case lNode.Level of
+  1:
+    begin
+    fpDoc.PopTill('package');
+    CheckIsValidName(lText);
+    FPDoc.Push(ElementNodeNames[etModule],lText);
+    end;
+  2:
+    begin
+    et:=GetElementType(lText);
+    CheckIsValidName(lText);
+    fpDoc.PopTill('module');
+    FPDoc.Push(ElementNodeNames[et],lText);
+    end;
+  3:
+    begin
+    fpDoc.PopTill('element');
+    st:=GetSectionType(lText);
+    FPDoc.PushSection(st);
+    end;
+  end;
+  // Renderer.RenderChildren(lNode);
+end;
+
+class function TFPDocMarkDownHeadingBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownHeadingBlock;
+end;
+
+initialization
+  TFPDocMarkDownHeadingBlockRenderer.RegisterRenderer(TMarkDownFPDocRenderer);
+  TFPDocParagraphBlockRenderer.RegisterRenderer(TMarkdownFPDocRenderer);
+  TFPDocMarkDownQuoteBlockRenderer.RegisterRenderer(TMarkdownFPDocRenderer);
+  TFPDocMarkDownTextBlockRenderer.RegisterRenderer(TMarkdownFPDocRenderer);
+  TFPDocMarkDownListBlockRenderer.RegisterRenderer(TMarkdownFPDocRenderer);
+  TFPDocMarkDownListItemBlockRenderer.RegisterRenderer(TMarkdownFPDocRenderer);
+  TFPDocMarkDownCodeBlockRenderer.RegisterRenderer(TMarkdownFPDocRenderer);
+  TFPDocMarkDownHeadingBlockRenderer.RegisterRenderer(TMarkdownFPDocRenderer);
+  TFPDocMarkDownThematicBreakBlockRenderer.RegisterRenderer(TMarkdownFPDocRenderer);
+  TFPDocMarkDownTableBlockRenderer.RegisterRenderer(TMarkdownFPDocRenderer);
+  TFPDocMarkDownTableRowBlockRenderer.RegisterRenderer(TMarkdownFPDocRenderer);
+  TFPDocMarkDownDocumentRenderer.RegisterRenderer(TMarkdownFPDocRenderer);
+  TFPDocMarkDownTextRenderer.RegisterRenderer(TMarkdownFPDocRenderer);
+end.
+

+ 2160 - 0
packages/fcl-md/src/markdown.htmlentities.pas

@@ -0,0 +1,2160 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown HTML entities list.
+
+    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.HTMLEntities;
+
+// generated automatically from https://html.spec.whatwg.org/entities.json
+
+interface
+
+Type
+  THTMLEntityDef = record
+    e : AnsiString;
+    u : Unicodestring;
+  end;
+  THTMLEntityDefList = Array of THTMLEntityDef;
+
+const
+  EntityDefList : THTMLEntityDefList = (
+    (e:'AElig'; u: #$003F),
+    (e:'AMP'; u: #$0026),
+    (e:'Aacute'; u: #$003F),
+    (e:'Abreve'; u: #$003F),
+    (e:'Acirc'; u: #$003F),
+    (e:'Acy'; u: #$003F),
+    (e:'Afr'; u: #$003F#$003F),
+    (e:'Agrave'; u: #$003F),
+    (e:'Alpha'; u: #$003F),
+    (e:'Amacr'; u: #$003F),
+    (e:'And'; u: #$003F),
+    (e:'Aogon'; u: #$003F),
+    (e:'Aopf'; u: #$003F#$003F),
+    (e:'ApplyFunction'; u: #$003F),
+    (e:'Aring'; u: #$003F),
+    (e:'Ascr'; u: #$003F#$003F),
+    (e:'Assign'; u: #$003F),
+    (e:'Atilde'; u: #$003F),
+    (e:'Auml'; u: #$003F),
+    (e:'Backslash'; u: #$003F),
+    (e:'Barv'; u: #$003F),
+    (e:'Barwed'; u: #$003F),
+    (e:'Bcy'; u: #$003F),
+    (e:'Because'; u: #$003F),
+    (e:'Bernoullis'; u: #$003F),
+    (e:'Beta'; u: #$003F),
+    (e:'Bfr'; u: #$003F#$003F),
+    (e:'Bopf'; u: #$003F#$003F),
+    (e:'Breve'; u: #$003F),
+    (e:'Bscr'; u: #$003F),
+    (e:'Bumpeq'; u: #$003F),
+    (e:'CHcy'; u: #$003F),
+    (e:'COPY'; u: #$003F),
+    (e:'Cacute'; u: #$003F),
+    (e:'Cap'; u: #$003F),
+    (e:'CapitalDifferentialD'; u: #$003F),
+    (e:'Cayleys'; u: #$003F),
+    (e:'Ccaron'; u: #$003F),
+    (e:'Ccedil'; u: #$003F),
+    (e:'Ccirc'; u: #$003F),
+    (e:'Cconint'; u: #$003F),
+    (e:'Cdot'; u: #$003F),
+    (e:'Cedilla'; u: #$003F),
+    (e:'CenterDot'; u: #$003F),
+    (e:'Cfr'; u: #$003F),
+    (e:'Chi'; u: #$003F),
+    (e:'CircleDot'; u: #$003F),
+    (e:'CircleMinus'; u: #$003F),
+    (e:'CirclePlus'; u: #$003F),
+    (e:'CircleTimes'; u: #$003F),
+    (e:'ClockwiseContourIntegral'; u: #$003F),
+    (e:'CloseCurlyDoubleQuote'; u: #$003F),
+    (e:'CloseCurlyQuote'; u: #$003F),
+    (e:'Colon'; u: #$003F),
+    (e:'Colone'; u: #$003F),
+    (e:'Congruent'; u: #$003F),
+    (e:'Conint'; u: #$003F),
+    (e:'ContourIntegral'; u: #$003F),
+    (e:'Copf'; u: #$003F),
+    (e:'Coproduct'; u: #$003F),
+    (e:'CounterClockwiseContourIntegral'; u: #$003F),
+    (e:'Cross'; u: #$003F),
+    (e:'Cscr'; u: #$003F#$003F),
+    (e:'Cup'; u: #$003F),
+    (e:'CupCap'; u: #$003F),
+    (e:'DD'; u: #$003F),
+    (e:'DDotrahd'; u: #$003F),
+    (e:'DJcy'; u: #$003F),
+    (e:'DScy'; u: #$003F),
+    (e:'DZcy'; u: #$003F),
+    (e:'Dagger'; u: #$003F),
+    (e:'Darr'; u: #$003F),
+    (e:'Dashv'; u: #$003F),
+    (e:'Dcaron'; u: #$003F),
+    (e:'Dcy'; u: #$003F),
+    (e:'Del'; u: #$003F),
+    (e:'Delta'; u: #$003F),
+    (e:'Dfr'; u: #$003F#$003F),
+    (e:'DiacriticalAcute'; u: #$003F),
+    (e:'DiacriticalDot'; u: #$003F),
+    (e:'DiacriticalDoubleAcute'; u: #$003F),
+    (e:'DiacriticalGrave'; u: #$0060),
+    (e:'DiacriticalTilde'; u: #$003F),
+    (e:'Diamond'; u: #$003F),
+    (e:'DifferentialD'; u: #$003F),
+    (e:'Dopf'; u: #$003F#$003F),
+    (e:'Dot'; u: #$003F),
+    (e:'DotDot'; u: #$003F),
+    (e:'DotEqual'; u: #$003F),
+    (e:'DoubleContourIntegral'; u: #$003F),
+    (e:'DoubleDot'; u: #$003F),
+    (e:'DoubleDownArrow'; u: #$003F),
+    (e:'DoubleLeftArrow'; u: #$003F),
+    (e:'DoubleLeftRightArrow'; u: #$003F),
+    (e:'DoubleLeftTee'; u: #$003F),
+    (e:'DoubleLongLeftArrow'; u: #$003F),
+    (e:'DoubleLongLeftRightArrow'; u: #$003F),
+    (e:'DoubleLongRightArrow'; u: #$003F),
+    (e:'DoubleRightArrow'; u: #$003F),
+    (e:'DoubleRightTee'; u: #$003F),
+    (e:'DoubleUpArrow'; u: #$003F),
+    (e:'DoubleUpDownArrow'; u: #$003F),
+    (e:'DoubleVerticalBar'; u: #$003F),
+    (e:'DownArrow'; u: #$003F),
+    (e:'DownArrowBar'; u: #$003F),
+    (e:'DownArrowUpArrow'; u: #$003F),
+    (e:'DownBreve'; u: #$003F),
+    (e:'DownLeftRightVector'; u: #$003F),
+    (e:'DownLeftTeeVector'; u: #$003F),
+    (e:'DownLeftVector'; u: #$003F),
+    (e:'DownLeftVectorBar'; u: #$003F),
+    (e:'DownRightTeeVector'; u: #$003F),
+    (e:'DownRightVector'; u: #$003F),
+    (e:'DownRightVectorBar'; u: #$003F),
+    (e:'DownTee'; u: #$003F),
+    (e:'DownTeeArrow'; u: #$003F),
+    (e:'Downarrow'; u: #$003F),
+    (e:'Dscr'; u: #$003F#$003F),
+    (e:'Dstrok'; u: #$003F),
+    (e:'ENG'; u: #$003F),
+    (e:'ETH'; u: #$003F),
+    (e:'Eacute'; u: #$003F),
+    (e:'Ecaron'; u: #$003F),
+    (e:'Ecirc'; u: #$003F),
+    (e:'Ecy'; u: #$003F),
+    (e:'Edot'; u: #$003F),
+    (e:'Efr'; u: #$003F#$003F),
+    (e:'Egrave'; u: #$003F),
+    (e:'Element'; u: #$003F),
+    (e:'Emacr'; u: #$003F),
+    (e:'EmptySmallSquare'; u: #$003F),
+    (e:'EmptyVerySmallSquare'; u: #$003F),
+    (e:'Eogon'; u: #$003F),
+    (e:'Eopf'; u: #$003F#$003F),
+    (e:'Epsilon'; u: #$003F),
+    (e:'Equal'; u: #$003F),
+    (e:'EqualTilde'; u: #$003F),
+    (e:'Equilibrium'; u: #$003F),
+    (e:'Escr'; u: #$003F),
+    (e:'Esim'; u: #$003F),
+    (e:'Eta'; u: #$003F),
+    (e:'Euml'; u: #$003F),
+    (e:'Exists'; u: #$003F),
+    (e:'ExponentialE'; u: #$003F),
+    (e:'Fcy'; u: #$003F),
+    (e:'Ffr'; u: #$003F#$003F),
+    (e:'FilledSmallSquare'; u: #$003F),
+    (e:'FilledVerySmallSquare'; u: #$003F),
+    (e:'Fopf'; u: #$003F#$003F),
+    (e:'ForAll'; u: #$003F),
+    (e:'Fouriertrf'; u: #$003F),
+    (e:'Fscr'; u: #$003F),
+    (e:'GJcy'; u: #$003F),
+    (e:'GT'; u: #$003E),
+    (e:'Gamma'; u: #$003F),
+    (e:'Gammad'; u: #$003F),
+    (e:'Gbreve'; u: #$003F),
+    (e:'Gcedil'; u: #$003F),
+    (e:'Gcirc'; u: #$003F),
+    (e:'Gcy'; u: #$003F),
+    (e:'Gdot'; u: #$003F),
+    (e:'Gfr'; u: #$003F#$003F),
+    (e:'Gg'; u: #$003F),
+    (e:'Gopf'; u: #$003F#$003F),
+    (e:'GreaterEqual'; u: #$003F),
+    (e:'GreaterEqualLess'; u: #$003F),
+    (e:'GreaterFullEqual'; u: #$003F),
+    (e:'GreaterGreater'; u: #$003F),
+    (e:'GreaterLess'; u: #$003F),
+    (e:'GreaterSlantEqual'; u: #$003F),
+    (e:'GreaterTilde'; u: #$003F),
+    (e:'Gscr'; u: #$003F#$003F),
+    (e:'Gt'; u: #$003F),
+    (e:'HARDcy'; u: #$003F),
+    (e:'Hacek'; u: #$003F),
+    (e:'Hat'; u: #$005E),
+    (e:'Hcirc'; u: #$003F),
+    (e:'Hfr'; u: #$003F),
+    (e:'HilbertSpace'; u: #$003F),
+    (e:'Hopf'; u: #$003F),
+    (e:'HorizontalLine'; u: #$003F),
+    (e:'Hscr'; u: #$003F),
+    (e:'Hstrok'; u: #$003F),
+    (e:'HumpDownHump'; u: #$003F),
+    (e:'HumpEqual'; u: #$003F),
+    (e:'IEcy'; u: #$003F),
+    (e:'IJlig'; u: #$003F),
+    (e:'IOcy'; u: #$003F),
+    (e:'Iacute'; u: #$003F),
+    (e:'Icirc'; u: #$003F),
+    (e:'Icy'; u: #$003F),
+    (e:'Idot'; u: #$003F),
+    (e:'Ifr'; u: #$003F),
+    (e:'Igrave'; u: #$003F),
+    (e:'Im'; u: #$003F),
+    (e:'Imacr'; u: #$003F),
+    (e:'ImaginaryI'; u: #$003F),
+    (e:'Implies'; u: #$003F),
+    (e:'Int'; u: #$003F),
+    (e:'Integral'; u: #$003F),
+    (e:'Intersection'; u: #$003F),
+    (e:'InvisibleComma'; u: #$003F),
+    (e:'InvisibleTimes'; u: #$003F),
+    (e:'Iogon'; u: #$003F),
+    (e:'Iopf'; u: #$003F#$003F),
+    (e:'Iota'; u: #$003F),
+    (e:'Iscr'; u: #$003F),
+    (e:'Itilde'; u: #$003F),
+    (e:'Iukcy'; u: #$003F),
+    (e:'Iuml'; u: #$003F),
+    (e:'Jcirc'; u: #$003F),
+    (e:'Jcy'; u: #$003F),
+    (e:'Jfr'; u: #$003F#$003F),
+    (e:'Jopf'; u: #$003F#$003F),
+    (e:'Jscr'; u: #$003F#$003F),
+    (e:'Jsercy'; u: #$003F),
+    (e:'Jukcy'; u: #$003F),
+    (e:'KHcy'; u: #$003F),
+    (e:'KJcy'; u: #$003F),
+    (e:'Kappa'; u: #$003F),
+    (e:'Kcedil'; u: #$003F),
+    (e:'Kcy'; u: #$003F),
+    (e:'Kfr'; u: #$003F#$003F),
+    (e:'Kopf'; u: #$003F#$003F),
+    (e:'Kscr'; u: #$003F#$003F),
+    (e:'LJcy'; u: #$003F),
+    (e:'LT'; u: #$003C),
+    (e:'Lacute'; u: #$003F),
+    (e:'Lambda'; u: #$003F),
+    (e:'Lang'; u: #$003F),
+    (e:'Laplacetrf'; u: #$003F),
+    (e:'Larr'; u: #$003F),
+    (e:'Lcaron'; u: #$003F),
+    (e:'Lcedil'; u: #$003F),
+    (e:'Lcy'; u: #$003F),
+    (e:'LeftAngleBracket'; u: #$003F),
+    (e:'LeftArrow'; u: #$003F),
+    (e:'LeftArrowBar'; u: #$003F),
+    (e:'LeftArrowRightArrow'; u: #$003F),
+    (e:'LeftCeiling'; u: #$003F),
+    (e:'LeftDoubleBracket'; u: #$003F),
+    (e:'LeftDownTeeVector'; u: #$003F),
+    (e:'LeftDownVector'; u: #$003F),
+    (e:'LeftDownVectorBar'; u: #$003F),
+    (e:'LeftFloor'; u: #$003F),
+    (e:'LeftRightArrow'; u: #$003F),
+    (e:'LeftRightVector'; u: #$003F),
+    (e:'LeftTee'; u: #$003F),
+    (e:'LeftTeeArrow'; u: #$003F),
+    (e:'LeftTeeVector'; u: #$003F),
+    (e:'LeftTriangle'; u: #$003F),
+    (e:'LeftTriangleBar'; u: #$003F),
+    (e:'LeftTriangleEqual'; u: #$003F),
+    (e:'LeftUpDownVector'; u: #$003F),
+    (e:'LeftUpTeeVector'; u: #$003F),
+    (e:'LeftUpVector'; u: #$003F),
+    (e:'LeftUpVectorBar'; u: #$003F),
+    (e:'LeftVector'; u: #$003F),
+    (e:'LeftVectorBar'; u: #$003F),
+    (e:'Leftarrow'; u: #$003F),
+    (e:'Leftrightarrow'; u: #$003F),
+    (e:'LessEqualGreater'; u: #$003F),
+    (e:'LessFullEqual'; u: #$003F),
+    (e:'LessGreater'; u: #$003F),
+    (e:'LessLess'; u: #$003F),
+    (e:'LessSlantEqual'; u: #$003F),
+    (e:'LessTilde'; u: #$003F),
+    (e:'Lfr'; u: #$003F#$003F),
+    (e:'Ll'; u: #$003F),
+    (e:'Lleftarrow'; u: #$003F),
+    (e:'Lmidot'; u: #$003F),
+    (e:'LongLeftArrow'; u: #$003F),
+    (e:'LongLeftRightArrow'; u: #$003F),
+    (e:'LongRightArrow'; u: #$003F),
+    (e:'Longleftarrow'; u: #$003F),
+    (e:'Longleftrightarrow'; u: #$003F),
+    (e:'Longrightarrow'; u: #$003F),
+    (e:'Lopf'; u: #$003F#$003F),
+    (e:'LowerLeftArrow'; u: #$003F),
+    (e:'LowerRightArrow'; u: #$003F),
+    (e:'Lscr'; u: #$003F),
+    (e:'Lsh'; u: #$003F),
+    (e:'Lstrok'; u: #$003F),
+    (e:'Lt'; u: #$003F),
+    (e:'Map'; u: #$003F),
+    (e:'Mcy'; u: #$003F),
+    (e:'MediumSpace'; u: #$003F),
+    (e:'Mellintrf'; u: #$003F),
+    (e:'Mfr'; u: #$003F#$003F),
+    (e:'MinusPlus'; u: #$003F),
+    (e:'Mopf'; u: #$003F#$003F),
+    (e:'Mscr'; u: #$003F),
+    (e:'Mu'; u: #$003F),
+    (e:'NJcy'; u: #$003F),
+    (e:'Nacute'; u: #$003F),
+    (e:'Ncaron'; u: #$003F),
+    (e:'Ncedil'; u: #$003F),
+    (e:'Ncy'; u: #$003F),
+    (e:'NegativeMediumSpace'; u: #$003F),
+    (e:'NegativeThickSpace'; u: #$003F),
+    (e:'NegativeThinSpace'; u: #$003F),
+    (e:'NegativeVeryThinSpace'; u: #$003F),
+    (e:'NestedGreaterGreater'; u: #$003F),
+    (e:'NestedLessLess'; u: #$003F),
+    (e:'NewLine'; u: #$000A),
+    (e:'Nfr'; u: #$003F#$003F),
+    (e:'NoBreak'; u: #$003F),
+    (e:'NonBreakingSpace'; u: #$003F),
+    (e:'Nopf'; u: #$003F),
+    (e:'Not'; u: #$003F),
+    (e:'NotCongruent'; u: #$003F),
+    (e:'NotCupCap'; u: #$003F),
+    (e:'NotDoubleVerticalBar'; u: #$003F),
+    (e:'NotElement'; u: #$003F),
+    (e:'NotEqual'; u: #$003F),
+    (e:'NotEqualTilde'; u: #$003F#$003F),
+    (e:'NotExists'; u: #$003F),
+    (e:'NotGreater'; u: #$003F),
+    (e:'NotGreaterEqual'; u: #$003F),
+    (e:'NotGreaterFullEqual'; u: #$003F#$003F),
+    (e:'NotGreaterGreater'; u: #$003F#$003F),
+    (e:'NotGreaterLess'; u: #$003F),
+    (e:'NotGreaterSlantEqual'; u: #$003F#$003F),
+    (e:'NotGreaterTilde'; u: #$003F),
+    (e:'NotHumpDownHump'; u: #$003F#$003F),
+    (e:'NotHumpEqual'; u: #$003F#$003F),
+    (e:'NotLeftTriangle'; u: #$003F),
+    (e:'NotLeftTriangleBar'; u: #$003F#$003F),
+    (e:'NotLeftTriangleEqual'; u: #$003F),
+    (e:'NotLess'; u: #$003F),
+    (e:'NotLessEqual'; u: #$003F),
+    (e:'NotLessGreater'; u: #$003F),
+    (e:'NotLessLess'; u: #$003F#$003F),
+    (e:'NotLessSlantEqual'; u: #$003F#$003F),
+    (e:'NotLessTilde'; u: #$003F),
+    (e:'NotNestedGreaterGreater'; u: #$003F#$003F),
+    (e:'NotNestedLessLess'; u: #$003F#$003F),
+    (e:'NotPrecedes'; u: #$003F),
+    (e:'NotPrecedesEqual'; u: #$003F#$003F),
+    (e:'NotPrecedesSlantEqual'; u: #$003F),
+    (e:'NotReverseElement'; u: #$003F),
+    (e:'NotRightTriangle'; u: #$003F),
+    (e:'NotRightTriangleBar'; u: #$003F#$003F),
+    (e:'NotRightTriangleEqual'; u: #$003F),
+    (e:'NotSquareSubset'; u: #$003F#$003F),
+    (e:'NotSquareSubsetEqual'; u: #$003F),
+    (e:'NotSquareSuperset'; u: #$003F#$003F),
+    (e:'NotSquareSupersetEqual'; u: #$003F),
+    (e:'NotSubset'; u: #$003F#$003F),
+    (e:'NotSubsetEqual'; u: #$003F),
+    (e:'NotSucceeds'; u: #$003F),
+    (e:'NotSucceedsEqual'; u: #$003F#$003F),
+    (e:'NotSucceedsSlantEqual'; u: #$003F),
+    (e:'NotSucceedsTilde'; u: #$003F#$003F),
+    (e:'NotSuperset'; u: #$003F#$003F),
+    (e:'NotSupersetEqual'; u: #$003F),
+    (e:'NotTilde'; u: #$003F),
+    (e:'NotTildeEqual'; u: #$003F),
+    (e:'NotTildeFullEqual'; u: #$003F),
+    (e:'NotTildeTilde'; u: #$003F),
+    (e:'NotVerticalBar'; u: #$003F),
+    (e:'Nscr'; u: #$003F#$003F),
+    (e:'Ntilde'; u: #$003F),
+    (e:'Nu'; u: #$003F),
+    (e:'OElig'; u: #$003F),
+    (e:'Oacute'; u: #$003F),
+    (e:'Ocirc'; u: #$003F),
+    (e:'Ocy'; u: #$003F),
+    (e:'Odblac'; u: #$003F),
+    (e:'Ofr'; u: #$003F#$003F),
+    (e:'Ograve'; u: #$003F),
+    (e:'Omacr'; u: #$003F),
+    (e:'Omega'; u: #$003F),
+    (e:'Omicron'; u: #$003F),
+    (e:'Oopf'; u: #$003F#$003F),
+    (e:'OpenCurlyDoubleQuote'; u: #$003F),
+    (e:'OpenCurlyQuote'; u: #$003F),
+    (e:'Or'; u: #$003F),
+    (e:'Oscr'; u: #$003F#$003F),
+    (e:'Oslash'; u: #$003F),
+    (e:'Otilde'; u: #$003F),
+    (e:'Otimes'; u: #$003F),
+    (e:'Ouml'; u: #$003F),
+    (e:'OverBar'; u: #$003F),
+    (e:'OverBrace'; u: #$003F),
+    (e:'OverBracket'; u: #$003F),
+    (e:'OverParenthesis'; u: #$003F),
+    (e:'PartialD'; u: #$003F),
+    (e:'Pcy'; u: #$003F),
+    (e:'Pfr'; u: #$003F#$003F),
+    (e:'Phi'; u: #$003F),
+    (e:'Pi'; u: #$003F),
+    (e:'PlusMinus'; u: #$003F),
+    (e:'Poincareplane'; u: #$003F),
+    (e:'Popf'; u: #$003F),
+    (e:'Pr'; u: #$003F),
+    (e:'Precedes'; u: #$003F),
+    (e:'PrecedesEqual'; u: #$003F),
+    (e:'PrecedesSlantEqual'; u: #$003F),
+    (e:'PrecedesTilde'; u: #$003F),
+    (e:'Prime'; u: #$003F),
+    (e:'Product'; u: #$003F),
+    (e:'Proportion'; u: #$003F),
+    (e:'Proportional'; u: #$003F),
+    (e:'Pscr'; u: #$003F#$003F),
+    (e:'Psi'; u: #$003F),
+    (e:'QUOT'; u: #$0022),
+    (e:'Qfr'; u: #$003F#$003F),
+    (e:'Qopf'; u: #$003F),
+    (e:'Qscr'; u: #$003F#$003F),
+    (e:'RBarr'; u: #$003F),
+    (e:'REG'; u: #$003F),
+    (e:'Racute'; u: #$003F),
+    (e:'Rang'; u: #$003F),
+    (e:'Rarr'; u: #$003F),
+    (e:'Rarrtl'; u: #$003F),
+    (e:'Rcaron'; u: #$003F),
+    (e:'Rcedil'; u: #$003F),
+    (e:'Rcy'; u: #$003F),
+    (e:'Re'; u: #$003F),
+    (e:'ReverseElement'; u: #$003F),
+    (e:'ReverseEquilibrium'; u: #$003F),
+    (e:'ReverseUpEquilibrium'; u: #$003F),
+    (e:'Rfr'; u: #$003F),
+    (e:'Rho'; u: #$003F),
+    (e:'RightAngleBracket'; u: #$003F),
+    (e:'RightArrow'; u: #$003F),
+    (e:'RightArrowBar'; u: #$003F),
+    (e:'RightArrowLeftArrow'; u: #$003F),
+    (e:'RightCeiling'; u: #$003F),
+    (e:'RightDoubleBracket'; u: #$003F),
+    (e:'RightDownTeeVector'; u: #$003F),
+    (e:'RightDownVector'; u: #$003F),
+    (e:'RightDownVectorBar'; u: #$003F),
+    (e:'RightFloor'; u: #$003F),
+    (e:'RightTee'; u: #$003F),
+    (e:'RightTeeArrow'; u: #$003F),
+    (e:'RightTeeVector'; u: #$003F),
+    (e:'RightTriangle'; u: #$003F),
+    (e:'RightTriangleBar'; u: #$003F),
+    (e:'RightTriangleEqual'; u: #$003F),
+    (e:'RightUpDownVector'; u: #$003F),
+    (e:'RightUpTeeVector'; u: #$003F),
+    (e:'RightUpVector'; u: #$003F),
+    (e:'RightUpVectorBar'; u: #$003F),
+    (e:'RightVector'; u: #$003F),
+    (e:'RightVectorBar'; u: #$003F),
+    (e:'Rightarrow'; u: #$003F),
+    (e:'Ropf'; u: #$003F),
+    (e:'RoundImplies'; u: #$003F),
+    (e:'Rrightarrow'; u: #$003F),
+    (e:'Rscr'; u: #$003F),
+    (e:'Rsh'; u: #$003F),
+    (e:'RuleDelayed'; u: #$003F),
+    (e:'SHCHcy'; u: #$003F),
+    (e:'SHcy'; u: #$003F),
+    (e:'SOFTcy'; u: #$003F),
+    (e:'Sacute'; u: #$003F),
+    (e:'Sc'; u: #$003F),
+    (e:'Scaron'; u: #$003F),
+    (e:'Scedil'; u: #$003F),
+    (e:'Scirc'; u: #$003F),
+    (e:'Scy'; u: #$003F),
+    (e:'Sfr'; u: #$003F#$003F),
+    (e:'ShortDownArrow'; u: #$003F),
+    (e:'ShortLeftArrow'; u: #$003F),
+    (e:'ShortRightArrow'; u: #$003F),
+    (e:'ShortUpArrow'; u: #$003F),
+    (e:'Sigma'; u: #$003F),
+    (e:'SmallCircle'; u: #$003F),
+    (e:'Sopf'; u: #$003F#$003F),
+    (e:'Sqrt'; u: #$003F),
+    (e:'Square'; u: #$003F),
+    (e:'SquareIntersection'; u: #$003F),
+    (e:'SquareSubset'; u: #$003F),
+    (e:'SquareSubsetEqual'; u: #$003F),
+    (e:'SquareSuperset'; u: #$003F),
+    (e:'SquareSupersetEqual'; u: #$003F),
+    (e:'SquareUnion'; u: #$003F),
+    (e:'Sscr'; u: #$003F#$003F),
+    (e:'Star'; u: #$003F),
+    (e:'Sub'; u: #$003F),
+    (e:'Subset'; u: #$003F),
+    (e:'SubsetEqual'; u: #$003F),
+    (e:'Succeeds'; u: #$003F),
+    (e:'SucceedsEqual'; u: #$003F),
+    (e:'SucceedsSlantEqual'; u: #$003F),
+    (e:'SucceedsTilde'; u: #$003F),
+    (e:'SuchThat'; u: #$003F),
+    (e:'Sum'; u: #$003F),
+    (e:'Sup'; u: #$003F),
+    (e:'Superset'; u: #$003F),
+    (e:'SupersetEqual'; u: #$003F),
+    (e:'Supset'; u: #$003F),
+    (e:'THORN'; u: #$003F),
+    (e:'TRADE'; u: #$003F),
+    (e:'TSHcy'; u: #$003F),
+    (e:'TScy'; u: #$003F),
+    (e:'Tab'; u: #$0009),
+    (e:'Tau'; u: #$003F),
+    (e:'Tcaron'; u: #$003F),
+    (e:'Tcedil'; u: #$003F),
+    (e:'Tcy'; u: #$003F),
+    (e:'Tfr'; u: #$003F#$003F),
+    (e:'Therefore'; u: #$003F),
+    (e:'Theta'; u: #$003F),
+    (e:'ThickSpace'; u: #$003F#$003F),
+    (e:'ThinSpace'; u: #$003F),
+    (e:'Tilde'; u: #$003F),
+    (e:'TildeEqual'; u: #$003F),
+    (e:'TildeFullEqual'; u: #$003F),
+    (e:'TildeTilde'; u: #$003F),
+    (e:'Topf'; u: #$003F#$003F),
+    (e:'TripleDot'; u: #$003F),
+    (e:'Tscr'; u: #$003F#$003F),
+    (e:'Tstrok'; u: #$003F),
+    (e:'Uacute'; u: #$003F),
+    (e:'Uarr'; u: #$003F),
+    (e:'Uarrocir'; u: #$003F),
+    (e:'Ubrcy'; u: #$003F),
+    (e:'Ubreve'; u: #$003F),
+    (e:'Ucirc'; u: #$003F),
+    (e:'Ucy'; u: #$003F),
+    (e:'Udblac'; u: #$003F),
+    (e:'Ufr'; u: #$003F#$003F),
+    (e:'Ugrave'; u: #$003F),
+    (e:'Umacr'; u: #$003F),
+    (e:'UnderBar'; u: #$005F),
+    (e:'UnderBrace'; u: #$003F),
+    (e:'UnderBracket'; u: #$003F),
+    (e:'UnderParenthesis'; u: #$003F),
+    (e:'Union'; u: #$003F),
+    (e:'UnionPlus'; u: #$003F),
+    (e:'Uogon'; u: #$003F),
+    (e:'Uopf'; u: #$003F#$003F),
+    (e:'UpArrow'; u: #$003F),
+    (e:'UpArrowBar'; u: #$003F),
+    (e:'UpArrowDownArrow'; u: #$003F),
+    (e:'UpDownArrow'; u: #$003F),
+    (e:'UpEquilibrium'; u: #$003F),
+    (e:'UpTee'; u: #$003F),
+    (e:'UpTeeArrow'; u: #$003F),
+    (e:'Uparrow'; u: #$003F),
+    (e:'Updownarrow'; u: #$003F),
+    (e:'UpperLeftArrow'; u: #$003F),
+    (e:'UpperRightArrow'; u: #$003F),
+    (e:'Upsi'; u: #$003F),
+    (e:'Upsilon'; u: #$003F),
+    (e:'Uring'; u: #$003F),
+    (e:'Uscr'; u: #$003F#$003F),
+    (e:'Utilde'; u: #$003F),
+    (e:'Uuml'; u: #$003F),
+    (e:'VDash'; u: #$003F),
+    (e:'Vbar'; u: #$003F),
+    (e:'Vcy'; u: #$003F),
+    (e:'Vdash'; u: #$003F),
+    (e:'Vdashl'; u: #$003F),
+    (e:'Vee'; u: #$003F),
+    (e:'Verbar'; u: #$003F),
+    (e:'Vert'; u: #$003F),
+    (e:'VerticalBar'; u: #$003F),
+    (e:'VerticalLine'; u: #$007C),
+    (e:'VerticalSeparator'; u: #$003F),
+    (e:'VerticalTilde'; u: #$003F),
+    (e:'VeryThinSpace'; u: #$003F),
+    (e:'Vfr'; u: #$003F#$003F),
+    (e:'Vopf'; u: #$003F#$003F),
+    (e:'Vscr'; u: #$003F#$003F),
+    (e:'Vvdash'; u: #$003F),
+    (e:'Wcirc'; u: #$003F),
+    (e:'Wedge'; u: #$003F),
+    (e:'Wfr'; u: #$003F#$003F),
+    (e:'Wopf'; u: #$003F#$003F),
+    (e:'Wscr'; u: #$003F#$003F),
+    (e:'Xfr'; u: #$003F#$003F),
+    (e:'Xi'; u: #$003F),
+    (e:'Xopf'; u: #$003F#$003F),
+    (e:'Xscr'; u: #$003F#$003F),
+    (e:'YAcy'; u: #$003F),
+    (e:'YIcy'; u: #$003F),
+    (e:'YUcy'; u: #$003F),
+    (e:'Yacute'; u: #$003F),
+    (e:'Ycirc'; u: #$003F),
+    (e:'Ycy'; u: #$003F),
+    (e:'Yfr'; u: #$003F#$003F),
+    (e:'Yopf'; u: #$003F#$003F),
+    (e:'Yscr'; u: #$003F#$003F),
+    (e:'Yuml'; u: #$003F),
+    (e:'ZHcy'; u: #$003F),
+    (e:'Zacute'; u: #$003F),
+    (e:'Zcaron'; u: #$003F),
+    (e:'Zcy'; u: #$003F),
+    (e:'Zdot'; u: #$003F),
+    (e:'ZeroWidthSpace'; u: #$003F),
+    (e:'Zeta'; u: #$003F),
+    (e:'Zfr'; u: #$003F),
+    (e:'Zopf'; u: #$003F),
+    (e:'Zscr'; u: #$003F#$003F),
+    (e:'aacute'; u: #$003F),
+    (e:'abreve'; u: #$003F),
+    (e:'ac'; u: #$003F),
+    (e:'acE'; u: #$003F#$003F),
+    (e:'acd'; u: #$003F),
+    (e:'acirc'; u: #$003F),
+    (e:'acute'; u: #$003F),
+    (e:'acy'; u: #$003F),
+    (e:'aelig'; u: #$003F),
+    (e:'af'; u: #$003F),
+    (e:'afr'; u: #$003F#$003F),
+    (e:'agrave'; u: #$003F),
+    (e:'alefsym'; u: #$003F),
+    (e:'aleph'; u: #$003F),
+    (e:'alpha'; u: #$003F),
+    (e:'amacr'; u: #$003F),
+    (e:'amalg'; u: #$003F),
+    (e:'amp'; u: #$0026),
+    (e:'and'; u: #$003F),
+    (e:'andand'; u: #$003F),
+    (e:'andd'; u: #$003F),
+    (e:'andslope'; u: #$003F),
+    (e:'andv'; u: #$003F),
+    (e:'ang'; u: #$003F),
+    (e:'ange'; u: #$003F),
+    (e:'angle'; u: #$003F),
+    (e:'angmsd'; u: #$003F),
+    (e:'angmsdaa'; u: #$003F),
+    (e:'angmsdab'; u: #$003F),
+    (e:'angmsdac'; u: #$003F),
+    (e:'angmsdad'; u: #$003F),
+    (e:'angmsdae'; u: #$003F),
+    (e:'angmsdaf'; u: #$003F),
+    (e:'angmsdag'; u: #$003F),
+    (e:'angmsdah'; u: #$003F),
+    (e:'angrt'; u: #$003F),
+    (e:'angrtvb'; u: #$003F),
+    (e:'angrtvbd'; u: #$003F),
+    (e:'angsph'; u: #$003F),
+    (e:'angst'; u: #$003F),
+    (e:'angzarr'; u: #$003F),
+    (e:'aogon'; u: #$003F),
+    (e:'aopf'; u: #$003F#$003F),
+    (e:'ap'; u: #$003F),
+    (e:'apE'; u: #$003F),
+    (e:'apacir'; u: #$003F),
+    (e:'ape'; u: #$003F),
+    (e:'apid'; u: #$003F),
+    (e:'apos'; u: #$0027),
+    (e:'approx'; u: #$003F),
+    (e:'approxeq'; u: #$003F),
+    (e:'aring'; u: #$003F),
+    (e:'ascr'; u: #$003F#$003F),
+    (e:'ast'; u: #$002A),
+    (e:'asymp'; u: #$003F),
+    (e:'asympeq'; u: #$003F),
+    (e:'atilde'; u: #$003F),
+    (e:'auml'; u: #$003F),
+    (e:'awconint'; u: #$003F),
+    (e:'awint'; u: #$003F),
+    (e:'bNot'; u: #$003F),
+    (e:'backcong'; u: #$003F),
+    (e:'backepsilon'; u: #$003F),
+    (e:'backprime'; u: #$003F),
+    (e:'backsim'; u: #$003F),
+    (e:'backsimeq'; u: #$003F),
+    (e:'barvee'; u: #$003F),
+    (e:'barwed'; u: #$003F),
+    (e:'barwedge'; u: #$003F),
+    (e:'bbrk'; u: #$003F),
+    (e:'bbrktbrk'; u: #$003F),
+    (e:'bcong'; u: #$003F),
+    (e:'bcy'; u: #$003F),
+    (e:'bdquo'; u: #$003F),
+    (e:'becaus'; u: #$003F),
+    (e:'because'; u: #$003F),
+    (e:'bemptyv'; u: #$003F),
+    (e:'bepsi'; u: #$003F),
+    (e:'bernou'; u: #$003F),
+    (e:'beta'; u: #$003F),
+    (e:'beth'; u: #$003F),
+    (e:'between'; u: #$003F),
+    (e:'bfr'; u: #$003F#$003F),
+    (e:'bigcap'; u: #$003F),
+    (e:'bigcirc'; u: #$003F),
+    (e:'bigcup'; u: #$003F),
+    (e:'bigodot'; u: #$003F),
+    (e:'bigoplus'; u: #$003F),
+    (e:'bigotimes'; u: #$003F),
+    (e:'bigsqcup'; u: #$003F),
+    (e:'bigstar'; u: #$003F),
+    (e:'bigtriangledown'; u: #$003F),
+    (e:'bigtriangleup'; u: #$003F),
+    (e:'biguplus'; u: #$003F),
+    (e:'bigvee'; u: #$003F),
+    (e:'bigwedge'; u: #$003F),
+    (e:'bkarow'; u: #$003F),
+    (e:'blacklozenge'; u: #$003F),
+    (e:'blacksquare'; u: #$003F),
+    (e:'blacktriangle'; u: #$003F),
+    (e:'blacktriangledown'; u: #$003F),
+    (e:'blacktriangleleft'; u: #$003F),
+    (e:'blacktriangleright'; u: #$003F),
+    (e:'blank'; u: #$003F),
+    (e:'blk12'; u: #$003F),
+    (e:'blk14'; u: #$003F),
+    (e:'blk34'; u: #$003F),
+    (e:'block'; u: #$003F),
+    (e:'bne'; u: #$003D#$003F),
+    (e:'bnequiv'; u: #$003F#$003F),
+    (e:'bnot'; u: #$003F),
+    (e:'bopf'; u: #$003F#$003F),
+    (e:'bot'; u: #$003F),
+    (e:'bottom'; u: #$003F),
+    (e:'bowtie'; u: #$003F),
+    (e:'boxDL'; u: #$003F),
+    (e:'boxDR'; u: #$003F),
+    (e:'boxDl'; u: #$003F),
+    (e:'boxDr'; u: #$003F),
+    (e:'boxH'; u: #$003F),
+    (e:'boxHD'; u: #$003F),
+    (e:'boxHU'; u: #$003F),
+    (e:'boxHd'; u: #$003F),
+    (e:'boxHu'; u: #$003F),
+    (e:'boxUL'; u: #$003F),
+    (e:'boxUR'; u: #$003F),
+    (e:'boxUl'; u: #$003F),
+    (e:'boxUr'; u: #$003F),
+    (e:'boxV'; u: #$003F),
+    (e:'boxVH'; u: #$003F),
+    (e:'boxVL'; u: #$003F),
+    (e:'boxVR'; u: #$003F),
+    (e:'boxVh'; u: #$003F),
+    (e:'boxVl'; u: #$003F),
+    (e:'boxVr'; u: #$003F),
+    (e:'boxbox'; u: #$003F),
+    (e:'boxdL'; u: #$003F),
+    (e:'boxdR'; u: #$003F),
+    (e:'boxdl'; u: #$003F),
+    (e:'boxdr'; u: #$003F),
+    (e:'boxh'; u: #$003F),
+    (e:'boxhD'; u: #$003F),
+    (e:'boxhU'; u: #$003F),
+    (e:'boxhd'; u: #$003F),
+    (e:'boxhu'; u: #$003F),
+    (e:'boxminus'; u: #$003F),
+    (e:'boxplus'; u: #$003F),
+    (e:'boxtimes'; u: #$003F),
+    (e:'boxuL'; u: #$003F),
+    (e:'boxuR'; u: #$003F),
+    (e:'boxul'; u: #$003F),
+    (e:'boxur'; u: #$003F),
+    (e:'boxv'; u: #$003F),
+    (e:'boxvH'; u: #$003F),
+    (e:'boxvL'; u: #$003F),
+    (e:'boxvR'; u: #$003F),
+    (e:'boxvh'; u: #$003F),
+    (e:'boxvl'; u: #$003F),
+    (e:'boxvr'; u: #$003F),
+    (e:'bprime'; u: #$003F),
+    (e:'breve'; u: #$003F),
+    (e:'brvbar'; u: #$003F),
+    (e:'bscr'; u: #$003F#$003F),
+    (e:'bsemi'; u: #$003F),
+    (e:'bsim'; u: #$003F),
+    (e:'bsime'; u: #$003F),
+    (e:'bsol'; u: #$005C),
+    (e:'bsolb'; u: #$003F),
+    (e:'bsolhsub'; u: #$003F),
+    (e:'bull'; u: #$003F),
+    (e:'bullet'; u: #$003F),
+    (e:'bump'; u: #$003F),
+    (e:'bumpE'; u: #$003F),
+    (e:'bumpe'; u: #$003F),
+    (e:'bumpeq'; u: #$003F),
+    (e:'cacute'; u: #$003F),
+    (e:'cap'; u: #$003F),
+    (e:'capand'; u: #$003F),
+    (e:'capbrcup'; u: #$003F),
+    (e:'capcap'; u: #$003F),
+    (e:'capcup'; u: #$003F),
+    (e:'capdot'; u: #$003F),
+    (e:'caps'; u: #$003F#$003F),
+    (e:'caret'; u: #$003F),
+    (e:'caron'; u: #$003F),
+    (e:'ccaps'; u: #$003F),
+    (e:'ccaron'; u: #$003F),
+    (e:'ccedil'; u: #$003F),
+    (e:'ccirc'; u: #$003F),
+    (e:'ccups'; u: #$003F),
+    (e:'ccupssm'; u: #$003F),
+    (e:'cdot'; u: #$003F),
+    (e:'cedil'; u: #$003F),
+    (e:'cemptyv'; u: #$003F),
+    (e:'cent'; u: #$003F),
+    (e:'centerdot'; u: #$003F),
+    (e:'cfr'; u: #$003F#$003F),
+    (e:'chcy'; u: #$003F),
+    (e:'check'; u: #$003F),
+    (e:'checkmark'; u: #$003F),
+    (e:'chi'; u: #$003F),
+    (e:'cir'; u: #$003F),
+    (e:'cirE'; u: #$003F),
+    (e:'circ'; u: #$003F),
+    (e:'circeq'; u: #$003F),
+    (e:'circlearrowleft'; u: #$003F),
+    (e:'circlearrowright'; u: #$003F),
+    (e:'circledR'; u: #$003F),
+    (e:'circledS'; u: #$003F),
+    (e:'circledast'; u: #$003F),
+    (e:'circledcirc'; u: #$003F),
+    (e:'circleddash'; u: #$003F),
+    (e:'cire'; u: #$003F),
+    (e:'cirfnint'; u: #$003F),
+    (e:'cirmid'; u: #$003F),
+    (e:'cirscir'; u: #$003F),
+    (e:'clubs'; u: #$003F),
+    (e:'clubsuit'; u: #$003F),
+    (e:'colon'; u: #$003A),
+    (e:'colone'; u: #$003F),
+    (e:'coloneq'; u: #$003F),
+    (e:'comma'; u: #$002C),
+    (e:'commat'; u: #$0040),
+    (e:'comp'; u: #$003F),
+    (e:'compfn'; u: #$003F),
+    (e:'complement'; u: #$003F),
+    (e:'complexes'; u: #$003F),
+    (e:'cong'; u: #$003F),
+    (e:'congdot'; u: #$003F),
+    (e:'conint'; u: #$003F),
+    (e:'copf'; u: #$003F#$003F),
+    (e:'coprod'; u: #$003F),
+    (e:'copy'; u: #$003F),
+    (e:'copysr'; u: #$003F),
+    (e:'crarr'; u: #$003F),
+    (e:'cross'; u: #$003F),
+    (e:'cscr'; u: #$003F#$003F),
+    (e:'csub'; u: #$003F),
+    (e:'csube'; u: #$003F),
+    (e:'csup'; u: #$003F),
+    (e:'csupe'; u: #$003F),
+    (e:'ctdot'; u: #$003F),
+    (e:'cudarrl'; u: #$003F),
+    (e:'cudarrr'; u: #$003F),
+    (e:'cuepr'; u: #$003F),
+    (e:'cuesc'; u: #$003F),
+    (e:'cularr'; u: #$003F),
+    (e:'cularrp'; u: #$003F),
+    (e:'cup'; u: #$003F),
+    (e:'cupbrcap'; u: #$003F),
+    (e:'cupcap'; u: #$003F),
+    (e:'cupcup'; u: #$003F),
+    (e:'cupdot'; u: #$003F),
+    (e:'cupor'; u: #$003F),
+    (e:'cups'; u: #$003F#$003F),
+    (e:'curarr'; u: #$003F),
+    (e:'curarrm'; u: #$003F),
+    (e:'curlyeqprec'; u: #$003F),
+    (e:'curlyeqsucc'; u: #$003F),
+    (e:'curlyvee'; u: #$003F),
+    (e:'curlywedge'; u: #$003F),
+    (e:'curren'; u: #$003F),
+    (e:'curvearrowleft'; u: #$003F),
+    (e:'curvearrowright'; u: #$003F),
+    (e:'cuvee'; u: #$003F),
+    (e:'cuwed'; u: #$003F),
+    (e:'cwconint'; u: #$003F),
+    (e:'cwint'; u: #$003F),
+    (e:'cylcty'; u: #$003F),
+    (e:'dArr'; u: #$003F),
+    (e:'dHar'; u: #$003F),
+    (e:'dagger'; u: #$003F),
+    (e:'daleth'; u: #$003F),
+    (e:'darr'; u: #$003F),
+    (e:'dash'; u: #$003F),
+    (e:'dashv'; u: #$003F),
+    (e:'dbkarow'; u: #$003F),
+    (e:'dblac'; u: #$003F),
+    (e:'dcaron'; u: #$003F),
+    (e:'dcy'; u: #$003F),
+    (e:'dd'; u: #$003F),
+    (e:'ddagger'; u: #$003F),
+    (e:'ddarr'; u: #$003F),
+    (e:'ddotseq'; u: #$003F),
+    (e:'deg'; u: #$003F),
+    (e:'delta'; u: #$003F),
+    (e:'demptyv'; u: #$003F),
+    (e:'dfisht'; u: #$003F),
+    (e:'dfr'; u: #$003F#$003F),
+    (e:'dharl'; u: #$003F),
+    (e:'dharr'; u: #$003F),
+    (e:'diam'; u: #$003F),
+    (e:'diamond'; u: #$003F),
+    (e:'diamondsuit'; u: #$003F),
+    (e:'diams'; u: #$003F),
+    (e:'die'; u: #$003F),
+    (e:'digamma'; u: #$003F),
+    (e:'disin'; u: #$003F),
+    (e:'div'; u: #$003F),
+    (e:'divide'; u: #$003F),
+    (e:'divideontimes'; u: #$003F),
+    (e:'divonx'; u: #$003F),
+    (e:'djcy'; u: #$003F),
+    (e:'dlcorn'; u: #$003F),
+    (e:'dlcrop'; u: #$003F),
+    (e:'dollar'; u: #$0024),
+    (e:'dopf'; u: #$003F#$003F),
+    (e:'dot'; u: #$003F),
+    (e:'doteq'; u: #$003F),
+    (e:'doteqdot'; u: #$003F),
+    (e:'dotminus'; u: #$003F),
+    (e:'dotplus'; u: #$003F),
+    (e:'dotsquare'; u: #$003F),
+    (e:'doublebarwedge'; u: #$003F),
+    (e:'downarrow'; u: #$003F),
+    (e:'downdownarrows'; u: #$003F),
+    (e:'downharpoonleft'; u: #$003F),
+    (e:'downharpoonright'; u: #$003F),
+    (e:'drbkarow'; u: #$003F),
+    (e:'drcorn'; u: #$003F),
+    (e:'drcrop'; u: #$003F),
+    (e:'dscr'; u: #$003F#$003F),
+    (e:'dscy'; u: #$003F),
+    (e:'dsol'; u: #$003F),
+    (e:'dstrok'; u: #$003F),
+    (e:'dtdot'; u: #$003F),
+    (e:'dtri'; u: #$003F),
+    (e:'dtrif'; u: #$003F),
+    (e:'duarr'; u: #$003F),
+    (e:'duhar'; u: #$003F),
+    (e:'dwangle'; u: #$003F),
+    (e:'dzcy'; u: #$003F),
+    (e:'dzigrarr'; u: #$003F),
+    (e:'eDDot'; u: #$003F),
+    (e:'eDot'; u: #$003F),
+    (e:'eacute'; u: #$003F),
+    (e:'easter'; u: #$003F),
+    (e:'ecaron'; u: #$003F),
+    (e:'ecir'; u: #$003F),
+    (e:'ecirc'; u: #$003F),
+    (e:'ecolon'; u: #$003F),
+    (e:'ecy'; u: #$003F),
+    (e:'edot'; u: #$003F),
+    (e:'ee'; u: #$003F),
+    (e:'efDot'; u: #$003F),
+    (e:'efr'; u: #$003F#$003F),
+    (e:'eg'; u: #$003F),
+    (e:'egrave'; u: #$003F),
+    (e:'egs'; u: #$003F),
+    (e:'egsdot'; u: #$003F),
+    (e:'el'; u: #$003F),
+    (e:'elinters'; u: #$003F),
+    (e:'ell'; u: #$003F),
+    (e:'els'; u: #$003F),
+    (e:'elsdot'; u: #$003F),
+    (e:'emacr'; u: #$003F),
+    (e:'empty'; u: #$003F),
+    (e:'emptyset'; u: #$003F),
+    (e:'emptyv'; u: #$003F),
+    (e:'emsp13'; u: #$003F),
+    (e:'emsp14'; u: #$003F),
+    (e:'emsp'; u: #$003F),
+    (e:'eng'; u: #$003F),
+    (e:'ensp'; u: #$003F),
+    (e:'eogon'; u: #$003F),
+    (e:'eopf'; u: #$003F#$003F),
+    (e:'epar'; u: #$003F),
+    (e:'eparsl'; u: #$003F),
+    (e:'eplus'; u: #$003F),
+    (e:'epsi'; u: #$003F),
+    (e:'epsilon'; u: #$003F),
+    (e:'epsiv'; u: #$003F),
+    (e:'eqcirc'; u: #$003F),
+    (e:'eqcolon'; u: #$003F),
+    (e:'eqsim'; u: #$003F),
+    (e:'eqslantgtr'; u: #$003F),
+    (e:'eqslantless'; u: #$003F),
+    (e:'equals'; u: #$003D),
+    (e:'equest'; u: #$003F),
+    (e:'equiv'; u: #$003F),
+    (e:'equivDD'; u: #$003F),
+    (e:'eqvparsl'; u: #$003F),
+    (e:'erDot'; u: #$003F),
+    (e:'erarr'; u: #$003F),
+    (e:'escr'; u: #$003F),
+    (e:'esdot'; u: #$003F),
+    (e:'esim'; u: #$003F),
+    (e:'eta'; u: #$003F),
+    (e:'eth'; u: #$003F),
+    (e:'euml'; u: #$003F),
+    (e:'euro'; u: #$003F),
+    (e:'excl'; u: #$0021),
+    (e:'exist'; u: #$003F),
+    (e:'expectation'; u: #$003F),
+    (e:'exponentiale'; u: #$003F),
+    (e:'fallingdotseq'; u: #$003F),
+    (e:'fcy'; u: #$003F),
+    (e:'female'; u: #$003F),
+    (e:'ffilig'; u: #$003F),
+    (e:'fflig'; u: #$003F),
+    (e:'ffllig'; u: #$003F),
+    (e:'ffr'; u: #$003F#$003F),
+    (e:'filig'; u: #$003F),
+    (e:'fjlig'; u: #$0066#$006A),
+    (e:'flat'; u: #$003F),
+    (e:'fllig'; u: #$003F),
+    (e:'fltns'; u: #$003F),
+    (e:'fnof'; u: #$003F),
+    (e:'fopf'; u: #$003F#$003F),
+    (e:'forall'; u: #$003F),
+    (e:'fork'; u: #$003F),
+    (e:'forkv'; u: #$003F),
+    (e:'fpartint'; u: #$003F),
+    (e:'frac12'; u: #$003F),
+    (e:'frac13'; u: #$003F),
+    (e:'frac14'; u: #$003F),
+    (e:'frac15'; u: #$003F),
+    (e:'frac16'; u: #$003F),
+    (e:'frac18'; u: #$003F),
+    (e:'frac23'; u: #$003F),
+    (e:'frac25'; u: #$003F),
+    (e:'frac34'; u: #$003F),
+    (e:'frac35'; u: #$003F),
+    (e:'frac38'; u: #$003F),
+    (e:'frac45'; u: #$003F),
+    (e:'frac56'; u: #$003F),
+    (e:'frac58'; u: #$003F),
+    (e:'frac78'; u: #$003F),
+    (e:'frasl'; u: #$003F),
+    (e:'frown'; u: #$003F),
+    (e:'fscr'; u: #$003F#$003F),
+    (e:'gE'; u: #$003F),
+    (e:'gEl'; u: #$003F),
+    (e:'gacute'; u: #$003F),
+    (e:'gamma'; u: #$003F),
+    (e:'gammad'; u: #$003F),
+    (e:'gap'; u: #$003F),
+    (e:'gbreve'; u: #$003F),
+    (e:'gcirc'; u: #$003F),
+    (e:'gcy'; u: #$003F),
+    (e:'gdot'; u: #$003F),
+    (e:'ge'; u: #$003F),
+    (e:'gel'; u: #$003F),
+    (e:'geq'; u: #$003F),
+    (e:'geqq'; u: #$003F),
+    (e:'geqslant'; u: #$003F),
+    (e:'ges'; u: #$003F),
+    (e:'gescc'; u: #$003F),
+    (e:'gesdot'; u: #$003F),
+    (e:'gesdoto'; u: #$003F),
+    (e:'gesdotol'; u: #$003F),
+    (e:'gesl'; u: #$003F#$003F),
+    (e:'gesles'; u: #$003F),
+    (e:'gfr'; u: #$003F#$003F),
+    (e:'gg'; u: #$003F),
+    (e:'ggg'; u: #$003F),
+    (e:'gimel'; u: #$003F),
+    (e:'gjcy'; u: #$003F),
+    (e:'gl'; u: #$003F),
+    (e:'glE'; u: #$003F),
+    (e:'gla'; u: #$003F),
+    (e:'glj'; u: #$003F),
+    (e:'gnE'; u: #$003F),
+    (e:'gnap'; u: #$003F),
+    (e:'gnapprox'; u: #$003F),
+    (e:'gne'; u: #$003F),
+    (e:'gneq'; u: #$003F),
+    (e:'gneqq'; u: #$003F),
+    (e:'gnsim'; u: #$003F),
+    (e:'gopf'; u: #$003F#$003F),
+    (e:'grave'; u: #$0060),
+    (e:'gscr'; u: #$003F),
+    (e:'gsim'; u: #$003F),
+    (e:'gsime'; u: #$003F),
+    (e:'gsiml'; u: #$003F),
+    (e:'gt'; u: #$003E),
+    (e:'gtcc'; u: #$003F),
+    (e:'gtcir'; u: #$003F),
+    (e:'gtdot'; u: #$003F),
+    (e:'gtlPar'; u: #$003F),
+    (e:'gtquest'; u: #$003F),
+    (e:'gtrapprox'; u: #$003F),
+    (e:'gtrarr'; u: #$003F),
+    (e:'gtrdot'; u: #$003F),
+    (e:'gtreqless'; u: #$003F),
+    (e:'gtreqqless'; u: #$003F),
+    (e:'gtrless'; u: #$003F),
+    (e:'gtrsim'; u: #$003F),
+    (e:'gvertneqq'; u: #$003F#$003F),
+    (e:'gvnE'; u: #$003F#$003F),
+    (e:'hArr'; u: #$003F),
+    (e:'hairsp'; u: #$003F),
+    (e:'half'; u: #$003F),
+    (e:'hamilt'; u: #$003F),
+    (e:'hardcy'; u: #$003F),
+    (e:'harr'; u: #$003F),
+    (e:'harrcir'; u: #$003F),
+    (e:'harrw'; u: #$003F),
+    (e:'hbar'; u: #$003F),
+    (e:'hcirc'; u: #$003F),
+    (e:'hearts'; u: #$003F),
+    (e:'heartsuit'; u: #$003F),
+    (e:'hellip'; u: #$003F),
+    (e:'hercon'; u: #$003F),
+    (e:'hfr'; u: #$003F#$003F),
+    (e:'hksearow'; u: #$003F),
+    (e:'hkswarow'; u: #$003F),
+    (e:'hoarr'; u: #$003F),
+    (e:'homtht'; u: #$003F),
+    (e:'hookleftarrow'; u: #$003F),
+    (e:'hookrightarrow'; u: #$003F),
+    (e:'hopf'; u: #$003F#$003F),
+    (e:'horbar'; u: #$003F),
+    (e:'hscr'; u: #$003F#$003F),
+    (e:'hslash'; u: #$003F),
+    (e:'hstrok'; u: #$003F),
+    (e:'hybull'; u: #$003F),
+    (e:'hyphen'; u: #$003F),
+    (e:'iacute'; u: #$003F),
+    (e:'ic'; u: #$003F),
+    (e:'icirc'; u: #$003F),
+    (e:'icy'; u: #$003F),
+    (e:'iecy'; u: #$003F),
+    (e:'iexcl'; u: #$003F),
+    (e:'iff'; u: #$003F),
+    (e:'ifr'; u: #$003F#$003F),
+    (e:'igrave'; u: #$003F),
+    (e:'ii'; u: #$003F),
+    (e:'iiiint'; u: #$003F),
+    (e:'iiint'; u: #$003F),
+    (e:'iinfin'; u: #$003F),
+    (e:'iiota'; u: #$003F),
+    (e:'ijlig'; u: #$003F),
+    (e:'imacr'; u: #$003F),
+    (e:'image'; u: #$003F),
+    (e:'imagline'; u: #$003F),
+    (e:'imagpart'; u: #$003F),
+    (e:'imath'; u: #$003F),
+    (e:'imof'; u: #$003F),
+    (e:'imped'; u: #$003F),
+    (e:'in'; u: #$003F),
+    (e:'incare'; u: #$003F),
+    (e:'infin'; u: #$003F),
+    (e:'infintie'; u: #$003F),
+    (e:'inodot'; u: #$003F),
+    (e:'int'; u: #$003F),
+    (e:'intcal'; u: #$003F),
+    (e:'integers'; u: #$003F),
+    (e:'intercal'; u: #$003F),
+    (e:'intlarhk'; u: #$003F),
+    (e:'intprod'; u: #$003F),
+    (e:'iocy'; u: #$003F),
+    (e:'iogon'; u: #$003F),
+    (e:'iopf'; u: #$003F#$003F),
+    (e:'iota'; u: #$003F),
+    (e:'iprod'; u: #$003F),
+    (e:'iquest'; u: #$003F),
+    (e:'iscr'; u: #$003F#$003F),
+    (e:'isin'; u: #$003F),
+    (e:'isinE'; u: #$003F),
+    (e:'isindot'; u: #$003F),
+    (e:'isins'; u: #$003F),
+    (e:'isinsv'; u: #$003F),
+    (e:'isinv'; u: #$003F),
+    (e:'it'; u: #$003F),
+    (e:'itilde'; u: #$003F),
+    (e:'iukcy'; u: #$003F),
+    (e:'iuml'; u: #$003F),
+    (e:'jcirc'; u: #$003F),
+    (e:'jcy'; u: #$003F),
+    (e:'jfr'; u: #$003F#$003F),
+    (e:'jmath'; u: #$003F),
+    (e:'jopf'; u: #$003F#$003F),
+    (e:'jscr'; u: #$003F#$003F),
+    (e:'jsercy'; u: #$003F),
+    (e:'jukcy'; u: #$003F),
+    (e:'kappa'; u: #$003F),
+    (e:'kappav'; u: #$003F),
+    (e:'kcedil'; u: #$003F),
+    (e:'kcy'; u: #$003F),
+    (e:'kfr'; u: #$003F#$003F),
+    (e:'kgreen'; u: #$003F),
+    (e:'khcy'; u: #$003F),
+    (e:'kjcy'; u: #$003F),
+    (e:'kopf'; u: #$003F#$003F),
+    (e:'kscr'; u: #$003F#$003F),
+    (e:'lAarr'; u: #$003F),
+    (e:'lArr'; u: #$003F),
+    (e:'lAtail'; u: #$003F),
+    (e:'lBarr'; u: #$003F),
+    (e:'lE'; u: #$003F),
+    (e:'lEg'; u: #$003F),
+    (e:'lHar'; u: #$003F),
+    (e:'lacute'; u: #$003F),
+    (e:'laemptyv'; u: #$003F),
+    (e:'lagran'; u: #$003F),
+    (e:'lambda'; u: #$003F),
+    (e:'lang'; u: #$003F),
+    (e:'langd'; u: #$003F),
+    (e:'langle'; u: #$003F),
+    (e:'lap'; u: #$003F),
+    (e:'laquo'; u: #$003F),
+    (e:'larr'; u: #$003F),
+    (e:'larrb'; u: #$003F),
+    (e:'larrbfs'; u: #$003F),
+    (e:'larrfs'; u: #$003F),
+    (e:'larrhk'; u: #$003F),
+    (e:'larrlp'; u: #$003F),
+    (e:'larrpl'; u: #$003F),
+    (e:'larrsim'; u: #$003F),
+    (e:'larrtl'; u: #$003F),
+    (e:'lat'; u: #$003F),
+    (e:'latail'; u: #$003F),
+    (e:'late'; u: #$003F),
+    (e:'lates'; u: #$003F#$003F),
+    (e:'lbarr'; u: #$003F),
+    (e:'lbbrk'; u: #$003F),
+    (e:'lbrace'; u: #$007B),
+    (e:'lbrack'; u: #$005B),
+    (e:'lbrke'; u: #$003F),
+    (e:'lbrksld'; u: #$003F),
+    (e:'lbrkslu'; u: #$003F),
+    (e:'lcaron'; u: #$003F),
+    (e:'lcedil'; u: #$003F),
+    (e:'lceil'; u: #$003F),
+    (e:'lcub'; u: #$007B),
+    (e:'lcy'; u: #$003F),
+    (e:'ldca'; u: #$003F),
+    (e:'ldquo'; u: #$003F),
+    (e:'ldquor'; u: #$003F),
+    (e:'ldrdhar'; u: #$003F),
+    (e:'ldrushar'; u: #$003F),
+    (e:'ldsh'; u: #$003F),
+    (e:'le'; u: #$003F),
+    (e:'leftarrow'; u: #$003F),
+    (e:'leftarrowtail'; u: #$003F),
+    (e:'leftharpoondown'; u: #$003F),
+    (e:'leftharpoonup'; u: #$003F),
+    (e:'leftleftarrows'; u: #$003F),
+    (e:'leftrightarrow'; u: #$003F),
+    (e:'leftrightarrows'; u: #$003F),
+    (e:'leftrightharpoons'; u: #$003F),
+    (e:'leftrightsquigarrow'; u: #$003F),
+    (e:'leftthreetimes'; u: #$003F),
+    (e:'leg'; u: #$003F),
+    (e:'leq'; u: #$003F),
+    (e:'leqq'; u: #$003F),
+    (e:'leqslant'; u: #$003F),
+    (e:'les'; u: #$003F),
+    (e:'lescc'; u: #$003F),
+    (e:'lesdot'; u: #$003F),
+    (e:'lesdoto'; u: #$003F),
+    (e:'lesdotor'; u: #$003F),
+    (e:'lesg'; u: #$003F#$003F),
+    (e:'lesges'; u: #$003F),
+    (e:'lessapprox'; u: #$003F),
+    (e:'lessdot'; u: #$003F),
+    (e:'lesseqgtr'; u: #$003F),
+    (e:'lesseqqgtr'; u: #$003F),
+    (e:'lessgtr'; u: #$003F),
+    (e:'lesssim'; u: #$003F),
+    (e:'lfisht'; u: #$003F),
+    (e:'lfloor'; u: #$003F),
+    (e:'lfr'; u: #$003F#$003F),
+    (e:'lg'; u: #$003F),
+    (e:'lgE'; u: #$003F),
+    (e:'lhard'; u: #$003F),
+    (e:'lharu'; u: #$003F),
+    (e:'lharul'; u: #$003F),
+    (e:'lhblk'; u: #$003F),
+    (e:'ljcy'; u: #$003F),
+    (e:'ll'; u: #$003F),
+    (e:'llarr'; u: #$003F),
+    (e:'llcorner'; u: #$003F),
+    (e:'llhard'; u: #$003F),
+    (e:'lltri'; u: #$003F),
+    (e:'lmidot'; u: #$003F),
+    (e:'lmoust'; u: #$003F),
+    (e:'lmoustache'; u: #$003F),
+    (e:'lnE'; u: #$003F),
+    (e:'lnap'; u: #$003F),
+    (e:'lnapprox'; u: #$003F),
+    (e:'lne'; u: #$003F),
+    (e:'lneq'; u: #$003F),
+    (e:'lneqq'; u: #$003F),
+    (e:'lnsim'; u: #$003F),
+    (e:'loang'; u: #$003F),
+    (e:'loarr'; u: #$003F),
+    (e:'lobrk'; u: #$003F),
+    (e:'longleftarrow'; u: #$003F),
+    (e:'longleftrightarrow'; u: #$003F),
+    (e:'longmapsto'; u: #$003F),
+    (e:'longrightarrow'; u: #$003F),
+    (e:'looparrowleft'; u: #$003F),
+    (e:'looparrowright'; u: #$003F),
+    (e:'lopar'; u: #$003F),
+    (e:'lopf'; u: #$003F#$003F),
+    (e:'loplus'; u: #$003F),
+    (e:'lotimes'; u: #$003F),
+    (e:'lowast'; u: #$003F),
+    (e:'lowbar'; u: #$005F),
+    (e:'loz'; u: #$003F),
+    (e:'lozenge'; u: #$003F),
+    (e:'lozf'; u: #$003F),
+    (e:'lpar'; u: #$0028),
+    (e:'lparlt'; u: #$003F),
+    (e:'lrarr'; u: #$003F),
+    (e:'lrcorner'; u: #$003F),
+    (e:'lrhar'; u: #$003F),
+    (e:'lrhard'; u: #$003F),
+    (e:'lrm'; u: #$003F),
+    (e:'lrtri'; u: #$003F),
+    (e:'lsaquo'; u: #$003F),
+    (e:'lscr'; u: #$003F#$003F),
+    (e:'lsh'; u: #$003F),
+    (e:'lsim'; u: #$003F),
+    (e:'lsime'; u: #$003F),
+    (e:'lsimg'; u: #$003F),
+    (e:'lsqb'; u: #$005B),
+    (e:'lsquo'; u: #$003F),
+    (e:'lsquor'; u: #$003F),
+    (e:'lstrok'; u: #$003F),
+    (e:'lt'; u: #$003C),
+    (e:'ltcc'; u: #$003F),
+    (e:'ltcir'; u: #$003F),
+    (e:'ltdot'; u: #$003F),
+    (e:'lthree'; u: #$003F),
+    (e:'ltimes'; u: #$003F),
+    (e:'ltlarr'; u: #$003F),
+    (e:'ltquest'; u: #$003F),
+    (e:'ltrPar'; u: #$003F),
+    (e:'ltri'; u: #$003F),
+    (e:'ltrie'; u: #$003F),
+    (e:'ltrif'; u: #$003F),
+    (e:'lurdshar'; u: #$003F),
+    (e:'luruhar'; u: #$003F),
+    (e:'lvertneqq'; u: #$003F#$003F),
+    (e:'lvnE'; u: #$003F#$003F),
+    (e:'mDDot'; u: #$003F),
+    (e:'macr'; u: #$003F),
+    (e:'male'; u: #$003F),
+    (e:'malt'; u: #$003F),
+    (e:'maltese'; u: #$003F),
+    (e:'map'; u: #$003F),
+    (e:'mapsto'; u: #$003F),
+    (e:'mapstodown'; u: #$003F),
+    (e:'mapstoleft'; u: #$003F),
+    (e:'mapstoup'; u: #$003F),
+    (e:'marker'; u: #$003F),
+    (e:'mcomma'; u: #$003F),
+    (e:'mcy'; u: #$003F),
+    (e:'mdash'; u: #$003F),
+    (e:'measuredangle'; u: #$003F),
+    (e:'mfr'; u: #$003F#$003F),
+    (e:'mho'; u: #$003F),
+    (e:'micro'; u: #$003F),
+    (e:'mid'; u: #$003F),
+    (e:'midast'; u: #$002A),
+    (e:'midcir'; u: #$003F),
+    (e:'middot'; u: #$003F),
+    (e:'minus'; u: #$003F),
+    (e:'minusb'; u: #$003F),
+    (e:'minusd'; u: #$003F),
+    (e:'minusdu'; u: #$003F),
+    (e:'mlcp'; u: #$003F),
+    (e:'mldr'; u: #$003F),
+    (e:'mnplus'; u: #$003F),
+    (e:'models'; u: #$003F),
+    (e:'mopf'; u: #$003F#$003F),
+    (e:'mp'; u: #$003F),
+    (e:'mscr'; u: #$003F#$003F),
+    (e:'mstpos'; u: #$003F),
+    (e:'mu'; u: #$003F),
+    (e:'multimap'; u: #$003F),
+    (e:'mumap'; u: #$003F),
+    (e:'nGg'; u: #$003F#$003F),
+    (e:'nGt'; u: #$003F#$003F),
+    (e:'nGtv'; u: #$003F#$003F),
+    (e:'nLeftarrow'; u: #$003F),
+    (e:'nLeftrightarrow'; u: #$003F),
+    (e:'nLl'; u: #$003F#$003F),
+    (e:'nLt'; u: #$003F#$003F),
+    (e:'nLtv'; u: #$003F#$003F),
+    (e:'nRightarrow'; u: #$003F),
+    (e:'nVDash'; u: #$003F),
+    (e:'nVdash'; u: #$003F),
+    (e:'nabla'; u: #$003F),
+    (e:'nacute'; u: #$003F),
+    (e:'nang'; u: #$003F#$003F),
+    (e:'nap'; u: #$003F),
+    (e:'napE'; u: #$003F#$003F),
+    (e:'napid'; u: #$003F#$003F),
+    (e:'napos'; u: #$003F),
+    (e:'napprox'; u: #$003F),
+    (e:'natur'; u: #$003F),
+    (e:'natural'; u: #$003F),
+    (e:'naturals'; u: #$003F),
+    (e:'nbsp'; u: #$003F),
+    (e:'nbump'; u: #$003F#$003F),
+    (e:'nbumpe'; u: #$003F#$003F),
+    (e:'ncap'; u: #$003F),
+    (e:'ncaron'; u: #$003F),
+    (e:'ncedil'; u: #$003F),
+    (e:'ncong'; u: #$003F),
+    (e:'ncongdot'; u: #$003F#$003F),
+    (e:'ncup'; u: #$003F),
+    (e:'ncy'; u: #$003F),
+    (e:'ndash'; u: #$003F),
+    (e:'ne'; u: #$003F),
+    (e:'neArr'; u: #$003F),
+    (e:'nearhk'; u: #$003F),
+    (e:'nearr'; u: #$003F),
+    (e:'nearrow'; u: #$003F),
+    (e:'nedot'; u: #$003F#$003F),
+    (e:'nequiv'; u: #$003F),
+    (e:'nesear'; u: #$003F),
+    (e:'nesim'; u: #$003F#$003F),
+    (e:'nexist'; u: #$003F),
+    (e:'nexists'; u: #$003F),
+    (e:'nfr'; u: #$003F#$003F),
+    (e:'ngE'; u: #$003F#$003F),
+    (e:'nge'; u: #$003F),
+    (e:'ngeq'; u: #$003F),
+    (e:'ngeqq'; u: #$003F#$003F),
+    (e:'ngeqslant'; u: #$003F#$003F),
+    (e:'nges'; u: #$003F#$003F),
+    (e:'ngsim'; u: #$003F),
+    (e:'ngt'; u: #$003F),
+    (e:'ngtr'; u: #$003F),
+    (e:'nhArr'; u: #$003F),
+    (e:'nharr'; u: #$003F),
+    (e:'nhpar'; u: #$003F),
+    (e:'ni'; u: #$003F),
+    (e:'nis'; u: #$003F),
+    (e:'nisd'; u: #$003F),
+    (e:'niv'; u: #$003F),
+    (e:'njcy'; u: #$003F),
+    (e:'nlArr'; u: #$003F),
+    (e:'nlE'; u: #$003F#$003F),
+    (e:'nlarr'; u: #$003F),
+    (e:'nldr'; u: #$003F),
+    (e:'nle'; u: #$003F),
+    (e:'nleftarrow'; u: #$003F),
+    (e:'nleftrightarrow'; u: #$003F),
+    (e:'nleq'; u: #$003F),
+    (e:'nleqq'; u: #$003F#$003F),
+    (e:'nleqslant'; u: #$003F#$003F),
+    (e:'nles'; u: #$003F#$003F),
+    (e:'nless'; u: #$003F),
+    (e:'nlsim'; u: #$003F),
+    (e:'nlt'; u: #$003F),
+    (e:'nltri'; u: #$003F),
+    (e:'nltrie'; u: #$003F),
+    (e:'nmid'; u: #$003F),
+    (e:'nopf'; u: #$003F#$003F),
+    (e:'not'; u: #$003F),
+    (e:'notin'; u: #$003F),
+    (e:'notinE'; u: #$003F#$003F),
+    (e:'notindot'; u: #$003F#$003F),
+    (e:'notinva'; u: #$003F),
+    (e:'notinvb'; u: #$003F),
+    (e:'notinvc'; u: #$003F),
+    (e:'notni'; u: #$003F),
+    (e:'notniva'; u: #$003F),
+    (e:'notnivb'; u: #$003F),
+    (e:'notnivc'; u: #$003F),
+    (e:'npar'; u: #$003F),
+    (e:'nparallel'; u: #$003F),
+    (e:'nparsl'; u: #$003F#$003F),
+    (e:'npart'; u: #$003F#$003F),
+    (e:'npolint'; u: #$003F),
+    (e:'npr'; u: #$003F),
+    (e:'nprcue'; u: #$003F),
+    (e:'npre'; u: #$003F#$003F),
+    (e:'nprec'; u: #$003F),
+    (e:'npreceq'; u: #$003F#$003F),
+    (e:'nrArr'; u: #$003F),
+    (e:'nrarr'; u: #$003F),
+    (e:'nrarrc'; u: #$003F#$003F),
+    (e:'nrarrw'; u: #$003F#$003F),
+    (e:'nrightarrow'; u: #$003F),
+    (e:'nrtri'; u: #$003F),
+    (e:'nrtrie'; u: #$003F),
+    (e:'nsc'; u: #$003F),
+    (e:'nsccue'; u: #$003F),
+    (e:'nsce'; u: #$003F#$003F),
+    (e:'nscr'; u: #$003F#$003F),
+    (e:'nshortmid'; u: #$003F),
+    (e:'nshortparallel'; u: #$003F),
+    (e:'nsim'; u: #$003F),
+    (e:'nsime'; u: #$003F),
+    (e:'nsimeq'; u: #$003F),
+    (e:'nsmid'; u: #$003F),
+    (e:'nspar'; u: #$003F),
+    (e:'nsqsube'; u: #$003F),
+    (e:'nsqsupe'; u: #$003F),
+    (e:'nsub'; u: #$003F),
+    (e:'nsubE'; u: #$003F#$003F),
+    (e:'nsube'; u: #$003F),
+    (e:'nsubset'; u: #$003F#$003F),
+    (e:'nsubseteq'; u: #$003F),
+    (e:'nsubseteqq'; u: #$003F#$003F),
+    (e:'nsucc'; u: #$003F),
+    (e:'nsucceq'; u: #$003F#$003F),
+    (e:'nsup'; u: #$003F),
+    (e:'nsupE'; u: #$003F#$003F),
+    (e:'nsupe'; u: #$003F),
+    (e:'nsupset'; u: #$003F#$003F),
+    (e:'nsupseteq'; u: #$003F),
+    (e:'nsupseteqq'; u: #$003F#$003F),
+    (e:'ntgl'; u: #$003F),
+    (e:'ntilde'; u: #$003F),
+    (e:'ntlg'; u: #$003F),
+    (e:'ntriangleleft'; u: #$003F),
+    (e:'ntrianglelefteq'; u: #$003F),
+    (e:'ntriangleright'; u: #$003F),
+    (e:'ntrianglerighteq'; u: #$003F),
+    (e:'nu'; u: #$003F),
+    (e:'num'; u: #$0023),
+    (e:'numero'; u: #$003F),
+    (e:'numsp'; u: #$003F),
+    (e:'nvDash'; u: #$003F),
+    (e:'nvHarr'; u: #$003F),
+    (e:'nvap'; u: #$003F#$003F),
+    (e:'nvdash'; u: #$003F),
+    (e:'nvge'; u: #$003F#$003F),
+    (e:'nvgt'; u: #$003E#$003F),
+    (e:'nvinfin'; u: #$003F),
+    (e:'nvlArr'; u: #$003F),
+    (e:'nvle'; u: #$003F#$003F),
+    (e:'nvlt'; u: #$003C#$003F),
+    (e:'nvltrie'; u: #$003F#$003F),
+    (e:'nvrArr'; u: #$003F),
+    (e:'nvrtrie'; u: #$003F#$003F),
+    (e:'nvsim'; u: #$003F#$003F),
+    (e:'nwArr'; u: #$003F),
+    (e:'nwarhk'; u: #$003F),
+    (e:'nwarr'; u: #$003F),
+    (e:'nwarrow'; u: #$003F),
+    (e:'nwnear'; u: #$003F),
+    (e:'oS'; u: #$003F),
+    (e:'oacute'; u: #$003F),
+    (e:'oast'; u: #$003F),
+    (e:'ocir'; u: #$003F),
+    (e:'ocirc'; u: #$003F),
+    (e:'ocy'; u: #$003F),
+    (e:'odash'; u: #$003F),
+    (e:'odblac'; u: #$003F),
+    (e:'odiv'; u: #$003F),
+    (e:'odot'; u: #$003F),
+    (e:'odsold'; u: #$003F),
+    (e:'oelig'; u: #$003F),
+    (e:'ofcir'; u: #$003F),
+    (e:'ofr'; u: #$003F#$003F),
+    (e:'ogon'; u: #$003F),
+    (e:'ograve'; u: #$003F),
+    (e:'ogt'; u: #$003F),
+    (e:'ohbar'; u: #$003F),
+    (e:'ohm'; u: #$003F),
+    (e:'oint'; u: #$003F),
+    (e:'olarr'; u: #$003F),
+    (e:'olcir'; u: #$003F),
+    (e:'olcross'; u: #$003F),
+    (e:'oline'; u: #$003F),
+    (e:'olt'; u: #$003F),
+    (e:'omacr'; u: #$003F),
+    (e:'omega'; u: #$003F),
+    (e:'omicron'; u: #$003F),
+    (e:'omid'; u: #$003F),
+    (e:'ominus'; u: #$003F),
+    (e:'oopf'; u: #$003F#$003F),
+    (e:'opar'; u: #$003F),
+    (e:'operp'; u: #$003F),
+    (e:'oplus'; u: #$003F),
+    (e:'or'; u: #$003F),
+    (e:'orarr'; u: #$003F),
+    (e:'ord'; u: #$003F),
+    (e:'order'; u: #$003F),
+    (e:'orderof'; u: #$003F),
+    (e:'ordf'; u: #$003F),
+    (e:'ordm'; u: #$003F),
+    (e:'origof'; u: #$003F),
+    (e:'oror'; u: #$003F),
+    (e:'orslope'; u: #$003F),
+    (e:'orv'; u: #$003F),
+    (e:'oscr'; u: #$003F),
+    (e:'oslash'; u: #$003F),
+    (e:'osol'; u: #$003F),
+    (e:'otilde'; u: #$003F),
+    (e:'otimes'; u: #$003F),
+    (e:'otimesas'; u: #$003F),
+    (e:'ouml'; u: #$003F),
+    (e:'ovbar'; u: #$003F),
+    (e:'par'; u: #$003F),
+    (e:'para'; u: #$003F),
+    (e:'parallel'; u: #$003F),
+    (e:'parsim'; u: #$003F),
+    (e:'parsl'; u: #$003F),
+    (e:'part'; u: #$003F),
+    (e:'pcy'; u: #$003F),
+    (e:'percnt'; u: #$0025),
+    (e:'period'; u: #$002E),
+    (e:'permil'; u: #$003F),
+    (e:'perp'; u: #$003F),
+    (e:'pertenk'; u: #$003F),
+    (e:'pfr'; u: #$003F#$003F),
+    (e:'phi'; u: #$003F),
+    (e:'phiv'; u: #$003F),
+    (e:'phmmat'; u: #$003F),
+    (e:'phone'; u: #$003F),
+    (e:'pi'; u: #$003F),
+    (e:'pitchfork'; u: #$003F),
+    (e:'piv'; u: #$003F),
+    (e:'planck'; u: #$003F),
+    (e:'planckh'; u: #$003F),
+    (e:'plankv'; u: #$003F),
+    (e:'plus'; u: #$002B),
+    (e:'plusacir'; u: #$003F),
+    (e:'plusb'; u: #$003F),
+    (e:'pluscir'; u: #$003F),
+    (e:'plusdo'; u: #$003F),
+    (e:'plusdu'; u: #$003F),
+    (e:'pluse'; u: #$003F),
+    (e:'plusmn'; u: #$003F),
+    (e:'plussim'; u: #$003F),
+    (e:'plustwo'; u: #$003F),
+    (e:'pm'; u: #$003F),
+    (e:'pointint'; u: #$003F),
+    (e:'popf'; u: #$003F#$003F),
+    (e:'pound'; u: #$003F),
+    (e:'pr'; u: #$003F),
+    (e:'prE'; u: #$003F),
+    (e:'prap'; u: #$003F),
+    (e:'prcue'; u: #$003F),
+    (e:'pre'; u: #$003F),
+    (e:'prec'; u: #$003F),
+    (e:'precapprox'; u: #$003F),
+    (e:'preccurlyeq'; u: #$003F),
+    (e:'preceq'; u: #$003F),
+    (e:'precnapprox'; u: #$003F),
+    (e:'precneqq'; u: #$003F),
+    (e:'precnsim'; u: #$003F),
+    (e:'precsim'; u: #$003F),
+    (e:'prime'; u: #$003F),
+    (e:'primes'; u: #$003F),
+    (e:'prnE'; u: #$003F),
+    (e:'prnap'; u: #$003F),
+    (e:'prnsim'; u: #$003F),
+    (e:'prod'; u: #$003F),
+    (e:'profalar'; u: #$003F),
+    (e:'profline'; u: #$003F),
+    (e:'profsurf'; u: #$003F),
+    (e:'prop'; u: #$003F),
+    (e:'propto'; u: #$003F),
+    (e:'prsim'; u: #$003F),
+    (e:'prurel'; u: #$003F),
+    (e:'pscr'; u: #$003F#$003F),
+    (e:'psi'; u: #$003F),
+    (e:'puncsp'; u: #$003F),
+    (e:'qfr'; u: #$003F#$003F),
+    (e:'qint'; u: #$003F),
+    (e:'qopf'; u: #$003F#$003F),
+    (e:'qprime'; u: #$003F),
+    (e:'qscr'; u: #$003F#$003F),
+    (e:'quaternions'; u: #$003F),
+    (e:'quatint'; u: #$003F),
+    (e:'quest'; u: #$003F),
+    (e:'questeq'; u: #$003F),
+    (e:'quot'; u: #$0022),
+    (e:'rAarr'; u: #$003F),
+    (e:'rArr'; u: #$003F),
+    (e:'rAtail'; u: #$003F),
+    (e:'rBarr'; u: #$003F),
+    (e:'rHar'; u: #$003F),
+    (e:'race'; u: #$003F#$003F),
+    (e:'racute'; u: #$003F),
+    (e:'radic'; u: #$003F),
+    (e:'raemptyv'; u: #$003F),
+    (e:'rang'; u: #$003F),
+    (e:'rangd'; u: #$003F),
+    (e:'range'; u: #$003F),
+    (e:'rangle'; u: #$003F),
+    (e:'raquo'; u: #$003F),
+    (e:'rarr'; u: #$003F),
+    (e:'rarrap'; u: #$003F),
+    (e:'rarrb'; u: #$003F),
+    (e:'rarrbfs'; u: #$003F),
+    (e:'rarrc'; u: #$003F),
+    (e:'rarrfs'; u: #$003F),
+    (e:'rarrhk'; u: #$003F),
+    (e:'rarrlp'; u: #$003F),
+    (e:'rarrpl'; u: #$003F),
+    (e:'rarrsim'; u: #$003F),
+    (e:'rarrtl'; u: #$003F),
+    (e:'rarrw'; u: #$003F),
+    (e:'ratail'; u: #$003F),
+    (e:'ratio'; u: #$003F),
+    (e:'rationals'; u: #$003F),
+    (e:'rbarr'; u: #$003F),
+    (e:'rbbrk'; u: #$003F),
+    (e:'rbrace'; u: #$007D),
+    (e:'rbrack'; u: #$005D),
+    (e:'rbrke'; u: #$003F),
+    (e:'rbrksld'; u: #$003F),
+    (e:'rbrkslu'; u: #$003F),
+    (e:'rcaron'; u: #$003F),
+    (e:'rcedil'; u: #$003F),
+    (e:'rceil'; u: #$003F),
+    (e:'rcub'; u: #$007D),
+    (e:'rcy'; u: #$003F),
+    (e:'rdca'; u: #$003F),
+    (e:'rdldhar'; u: #$003F),
+    (e:'rdquo'; u: #$003F),
+    (e:'rdquor'; u: #$003F),
+    (e:'rdsh'; u: #$003F),
+    (e:'real'; u: #$003F),
+    (e:'realine'; u: #$003F),
+    (e:'realpart'; u: #$003F),
+    (e:'reals'; u: #$003F),
+    (e:'rect'; u: #$003F),
+    (e:'reg'; u: #$003F),
+    (e:'rfisht'; u: #$003F),
+    (e:'rfloor'; u: #$003F),
+    (e:'rfr'; u: #$003F#$003F),
+    (e:'rhard'; u: #$003F),
+    (e:'rharu'; u: #$003F),
+    (e:'rharul'; u: #$003F),
+    (e:'rho'; u: #$003F),
+    (e:'rhov'; u: #$003F),
+    (e:'rightarrow'; u: #$003F),
+    (e:'rightarrowtail'; u: #$003F),
+    (e:'rightharpoondown'; u: #$003F),
+    (e:'rightharpoonup'; u: #$003F),
+    (e:'rightleftarrows'; u: #$003F),
+    (e:'rightleftharpoons'; u: #$003F),
+    (e:'rightrightarrows'; u: #$003F),
+    (e:'rightsquigarrow'; u: #$003F),
+    (e:'rightthreetimes'; u: #$003F),
+    (e:'ring'; u: #$003F),
+    (e:'risingdotseq'; u: #$003F),
+    (e:'rlarr'; u: #$003F),
+    (e:'rlhar'; u: #$003F),
+    (e:'rlm'; u: #$003F),
+    (e:'rmoust'; u: #$003F),
+    (e:'rmoustache'; u: #$003F),
+    (e:'rnmid'; u: #$003F),
+    (e:'roang'; u: #$003F),
+    (e:'roarr'; u: #$003F),
+    (e:'robrk'; u: #$003F),
+    (e:'ropar'; u: #$003F),
+    (e:'ropf'; u: #$003F#$003F),
+    (e:'roplus'; u: #$003F),
+    (e:'rotimes'; u: #$003F),
+    (e:'rpar'; u: #$0029),
+    (e:'rpargt'; u: #$003F),
+    (e:'rppolint'; u: #$003F),
+    (e:'rrarr'; u: #$003F),
+    (e:'rsaquo'; u: #$003F),
+    (e:'rscr'; u: #$003F#$003F),
+    (e:'rsh'; u: #$003F),
+    (e:'rsqb'; u: #$005D),
+    (e:'rsquo'; u: #$003F),
+    (e:'rsquor'; u: #$003F),
+    (e:'rthree'; u: #$003F),
+    (e:'rtimes'; u: #$003F),
+    (e:'rtri'; u: #$003F),
+    (e:'rtrie'; u: #$003F),
+    (e:'rtrif'; u: #$003F),
+    (e:'rtriltri'; u: #$003F),
+    (e:'ruluhar'; u: #$003F),
+    (e:'rx'; u: #$003F),
+    (e:'sacute'; u: #$003F),
+    (e:'sbquo'; u: #$003F),
+    (e:'sc'; u: #$003F),
+    (e:'scE'; u: #$003F),
+    (e:'scap'; u: #$003F),
+    (e:'scaron'; u: #$003F),
+    (e:'sccue'; u: #$003F),
+    (e:'sce'; u: #$003F),
+    (e:'scedil'; u: #$003F),
+    (e:'scirc'; u: #$003F),
+    (e:'scnE'; u: #$003F),
+    (e:'scnap'; u: #$003F),
+    (e:'scnsim'; u: #$003F),
+    (e:'scpolint'; u: #$003F),
+    (e:'scsim'; u: #$003F),
+    (e:'scy'; u: #$003F),
+    (e:'sdot'; u: #$003F),
+    (e:'sdotb'; u: #$003F),
+    (e:'sdote'; u: #$003F),
+    (e:'seArr'; u: #$003F),
+    (e:'searhk'; u: #$003F),
+    (e:'searr'; u: #$003F),
+    (e:'searrow'; u: #$003F),
+    (e:'sect'; u: #$003F),
+    (e:'semi'; u: #$003B),
+    (e:'seswar'; u: #$003F),
+    (e:'setminus'; u: #$003F),
+    (e:'setmn'; u: #$003F),
+    (e:'sext'; u: #$003F),
+    (e:'sfr'; u: #$003F#$003F),
+    (e:'sfrown'; u: #$003F),
+    (e:'sharp'; u: #$003F),
+    (e:'shchcy'; u: #$003F),
+    (e:'shcy'; u: #$003F),
+    (e:'shortmid'; u: #$003F),
+    (e:'shortparallel'; u: #$003F),
+    (e:'shy'; u: #$003F),
+    (e:'sigma'; u: #$003F),
+    (e:'sigmaf'; u: #$003F),
+    (e:'sigmav'; u: #$003F),
+    (e:'sim'; u: #$003F),
+    (e:'simdot'; u: #$003F),
+    (e:'sime'; u: #$003F),
+    (e:'simeq'; u: #$003F),
+    (e:'simg'; u: #$003F),
+    (e:'simgE'; u: #$003F),
+    (e:'siml'; u: #$003F),
+    (e:'simlE'; u: #$003F),
+    (e:'simne'; u: #$003F),
+    (e:'simplus'; u: #$003F),
+    (e:'simrarr'; u: #$003F),
+    (e:'slarr'; u: #$003F),
+    (e:'smallsetminus'; u: #$003F),
+    (e:'smashp'; u: #$003F),
+    (e:'smeparsl'; u: #$003F),
+    (e:'smid'; u: #$003F),
+    (e:'smile'; u: #$003F),
+    (e:'smt'; u: #$003F),
+    (e:'smte'; u: #$003F),
+    (e:'smtes'; u: #$003F#$003F),
+    (e:'softcy'; u: #$003F),
+    (e:'sol'; u: #$002F),
+    (e:'solb'; u: #$003F),
+    (e:'solbar'; u: #$003F),
+    (e:'sopf'; u: #$003F#$003F),
+    (e:'spades'; u: #$003F),
+    (e:'spadesuit'; u: #$003F),
+    (e:'spar'; u: #$003F),
+    (e:'sqcap'; u: #$003F),
+    (e:'sqcaps'; u: #$003F#$003F),
+    (e:'sqcup'; u: #$003F),
+    (e:'sqcups'; u: #$003F#$003F),
+    (e:'sqsub'; u: #$003F),
+    (e:'sqsube'; u: #$003F),
+    (e:'sqsubset'; u: #$003F),
+    (e:'sqsubseteq'; u: #$003F),
+    (e:'sqsup'; u: #$003F),
+    (e:'sqsupe'; u: #$003F),
+    (e:'sqsupset'; u: #$003F),
+    (e:'sqsupseteq'; u: #$003F),
+    (e:'squ'; u: #$003F),
+    (e:'square'; u: #$003F),
+    (e:'squarf'; u: #$003F),
+    (e:'squf'; u: #$003F),
+    (e:'srarr'; u: #$003F),
+    (e:'sscr'; u: #$003F#$003F),
+    (e:'ssetmn'; u: #$003F),
+    (e:'ssmile'; u: #$003F),
+    (e:'sstarf'; u: #$003F),
+    (e:'star'; u: #$003F),
+    (e:'starf'; u: #$003F),
+    (e:'straightepsilon'; u: #$003F),
+    (e:'straightphi'; u: #$003F),
+    (e:'strns'; u: #$003F),
+    (e:'sub'; u: #$003F),
+    (e:'subE'; u: #$003F),
+    (e:'subdot'; u: #$003F),
+    (e:'sube'; u: #$003F),
+    (e:'subedot'; u: #$003F),
+    (e:'submult'; u: #$003F),
+    (e:'subnE'; u: #$003F),
+    (e:'subne'; u: #$003F),
+    (e:'subplus'; u: #$003F),
+    (e:'subrarr'; u: #$003F),
+    (e:'subset'; u: #$003F),
+    (e:'subseteq'; u: #$003F),
+    (e:'subseteqq'; u: #$003F),
+    (e:'subsetneq'; u: #$003F),
+    (e:'subsetneqq'; u: #$003F),
+    (e:'subsim'; u: #$003F),
+    (e:'subsub'; u: #$003F),
+    (e:'subsup'; u: #$003F),
+    (e:'succ'; u: #$003F),
+    (e:'succapprox'; u: #$003F),
+    (e:'succcurlyeq'; u: #$003F),
+    (e:'succeq'; u: #$003F),
+    (e:'succnapprox'; u: #$003F),
+    (e:'succneqq'; u: #$003F),
+    (e:'succnsim'; u: #$003F),
+    (e:'succsim'; u: #$003F),
+    (e:'sum'; u: #$003F),
+    (e:'sung'; u: #$003F),
+    (e:'sup1'; u: #$003F),
+    (e:'sup2'; u: #$003F),
+    (e:'sup3'; u: #$003F),
+    (e:'sup'; u: #$003F),
+    (e:'supE'; u: #$003F),
+    (e:'supdot'; u: #$003F),
+    (e:'supdsub'; u: #$003F),
+    (e:'supe'; u: #$003F),
+    (e:'supedot'; u: #$003F),
+    (e:'suphsol'; u: #$003F),
+    (e:'suphsub'; u: #$003F),
+    (e:'suplarr'; u: #$003F),
+    (e:'supmult'; u: #$003F),
+    (e:'supnE'; u: #$003F),
+    (e:'supne'; u: #$003F),
+    (e:'supplus'; u: #$003F),
+    (e:'supset'; u: #$003F),
+    (e:'supseteq'; u: #$003F),
+    (e:'supseteqq'; u: #$003F),
+    (e:'supsetneq'; u: #$003F),
+    (e:'supsetneqq'; u: #$003F),
+    (e:'supsim'; u: #$003F),
+    (e:'supsub'; u: #$003F),
+    (e:'supsup'; u: #$003F),
+    (e:'swArr'; u: #$003F),
+    (e:'swarhk'; u: #$003F),
+    (e:'swarr'; u: #$003F),
+    (e:'swarrow'; u: #$003F),
+    (e:'swnwar'; u: #$003F),
+    (e:'szlig'; u: #$003F),
+    (e:'target'; u: #$003F),
+    (e:'tau'; u: #$003F),
+    (e:'tbrk'; u: #$003F),
+    (e:'tcaron'; u: #$003F),
+    (e:'tcedil'; u: #$003F),
+    (e:'tcy'; u: #$003F),
+    (e:'tdot'; u: #$003F),
+    (e:'telrec'; u: #$003F),
+    (e:'tfr'; u: #$003F#$003F),
+    (e:'there4'; u: #$003F),
+    (e:'therefore'; u: #$003F),
+    (e:'theta'; u: #$003F),
+    (e:'thetasym'; u: #$003F),
+    (e:'thetav'; u: #$003F),
+    (e:'thickapprox'; u: #$003F),
+    (e:'thicksim'; u: #$003F),
+    (e:'thinsp'; u: #$003F),
+    (e:'thkap'; u: #$003F),
+    (e:'thksim'; u: #$003F),
+    (e:'thorn'; u: #$003F),
+    (e:'tilde'; u: #$003F),
+    (e:'times'; u: #$003F),
+    (e:'timesb'; u: #$003F),
+    (e:'timesbar'; u: #$003F),
+    (e:'timesd'; u: #$003F),
+    (e:'tint'; u: #$003F),
+    (e:'toea'; u: #$003F),
+    (e:'top'; u: #$003F),
+    (e:'topbot'; u: #$003F),
+    (e:'topcir'; u: #$003F),
+    (e:'topf'; u: #$003F#$003F),
+    (e:'topfork'; u: #$003F),
+    (e:'tosa'; u: #$003F),
+    (e:'tprime'; u: #$003F),
+    (e:'trade'; u: #$003F),
+    (e:'triangle'; u: #$003F),
+    (e:'triangledown'; u: #$003F),
+    (e:'triangleleft'; u: #$003F),
+    (e:'trianglelefteq'; u: #$003F),
+    (e:'triangleq'; u: #$003F),
+    (e:'triangleright'; u: #$003F),
+    (e:'trianglerighteq'; u: #$003F),
+    (e:'tridot'; u: #$003F),
+    (e:'trie'; u: #$003F),
+    (e:'triminus'; u: #$003F),
+    (e:'triplus'; u: #$003F),
+    (e:'trisb'; u: #$003F),
+    (e:'tritime'; u: #$003F),
+    (e:'trpezium'; u: #$003F),
+    (e:'tscr'; u: #$003F#$003F),
+    (e:'tscy'; u: #$003F),
+    (e:'tshcy'; u: #$003F),
+    (e:'tstrok'; u: #$003F),
+    (e:'twixt'; u: #$003F),
+    (e:'twoheadleftarrow'; u: #$003F),
+    (e:'twoheadrightarrow'; u: #$003F),
+    (e:'uArr'; u: #$003F),
+    (e:'uHar'; u: #$003F),
+    (e:'uacute'; u: #$003F),
+    (e:'uarr'; u: #$003F),
+    (e:'ubrcy'; u: #$003F),
+    (e:'ubreve'; u: #$003F),
+    (e:'ucirc'; u: #$003F),
+    (e:'ucy'; u: #$003F),
+    (e:'udarr'; u: #$003F),
+    (e:'udblac'; u: #$003F),
+    (e:'udhar'; u: #$003F),
+    (e:'ufisht'; u: #$003F),
+    (e:'ufr'; u: #$003F#$003F),
+    (e:'ugrave'; u: #$003F),
+    (e:'uharl'; u: #$003F),
+    (e:'uharr'; u: #$003F),
+    (e:'uhblk'; u: #$003F),
+    (e:'ulcorn'; u: #$003F),
+    (e:'ulcorner'; u: #$003F),
+    (e:'ulcrop'; u: #$003F),
+    (e:'ultri'; u: #$003F),
+    (e:'umacr'; u: #$003F),
+    (e:'uml'; u: #$003F),
+    (e:'uogon'; u: #$003F),
+    (e:'uopf'; u: #$003F#$003F),
+    (e:'uparrow'; u: #$003F),
+    (e:'updownarrow'; u: #$003F),
+    (e:'upharpoonleft'; u: #$003F),
+    (e:'upharpoonright'; u: #$003F),
+    (e:'uplus'; u: #$003F),
+    (e:'upsi'; u: #$003F),
+    (e:'upsih'; u: #$003F),
+    (e:'upsilon'; u: #$003F),
+    (e:'upuparrows'; u: #$003F),
+    (e:'urcorn'; u: #$003F),
+    (e:'urcorner'; u: #$003F),
+    (e:'urcrop'; u: #$003F),
+    (e:'uring'; u: #$003F),
+    (e:'urtri'; u: #$003F),
+    (e:'uscr'; u: #$003F#$003F),
+    (e:'utdot'; u: #$003F),
+    (e:'utilde'; u: #$003F),
+    (e:'utri'; u: #$003F),
+    (e:'utrif'; u: #$003F),
+    (e:'uuarr'; u: #$003F),
+    (e:'uuml'; u: #$003F),
+    (e:'uwangle'; u: #$003F),
+    (e:'vArr'; u: #$003F),
+    (e:'vBar'; u: #$003F),
+    (e:'vBarv'; u: #$003F),
+    (e:'vDash'; u: #$003F),
+    (e:'vangrt'; u: #$003F),
+    (e:'varepsilon'; u: #$003F),
+    (e:'varkappa'; u: #$003F),
+    (e:'varnothing'; u: #$003F),
+    (e:'varphi'; u: #$003F),
+    (e:'varpi'; u: #$003F),
+    (e:'varpropto'; u: #$003F),
+    (e:'varr'; u: #$003F),
+    (e:'varrho'; u: #$003F),
+    (e:'varsigma'; u: #$003F),
+    (e:'varsubsetneq'; u: #$003F#$003F),
+    (e:'varsubsetneqq'; u: #$003F#$003F),
+    (e:'varsupsetneq'; u: #$003F#$003F),
+    (e:'varsupsetneqq'; u: #$003F#$003F),
+    (e:'vartheta'; u: #$003F),
+    (e:'vartriangleleft'; u: #$003F),
+    (e:'vartriangleright'; u: #$003F),
+    (e:'vcy'; u: #$003F),
+    (e:'vdash'; u: #$003F),
+    (e:'vee'; u: #$003F),
+    (e:'veebar'; u: #$003F),
+    (e:'veeeq'; u: #$003F),
+    (e:'vellip'; u: #$003F),
+    (e:'verbar'; u: #$007C),
+    (e:'vert'; u: #$007C),
+    (e:'vfr'; u: #$003F#$003F),
+    (e:'vltri'; u: #$003F),
+    (e:'vnsub'; u: #$003F#$003F),
+    (e:'vnsup'; u: #$003F#$003F),
+    (e:'vopf'; u: #$003F#$003F),
+    (e:'vprop'; u: #$003F),
+    (e:'vrtri'; u: #$003F),
+    (e:'vscr'; u: #$003F#$003F),
+    (e:'vsubnE'; u: #$003F#$003F),
+    (e:'vsubne'; u: #$003F#$003F),
+    (e:'vsupnE'; u: #$003F#$003F),
+    (e:'vsupne'; u: #$003F#$003F),
+    (e:'vzigzag'; u: #$003F),
+    (e:'wcirc'; u: #$003F),
+    (e:'wedbar'; u: #$003F),
+    (e:'wedge'; u: #$003F),
+    (e:'wedgeq'; u: #$003F),
+    (e:'weierp'; u: #$003F),
+    (e:'wfr'; u: #$003F#$003F),
+    (e:'wopf'; u: #$003F#$003F),
+    (e:'wp'; u: #$003F),
+    (e:'wr'; u: #$003F),
+    (e:'wreath'; u: #$003F),
+    (e:'wscr'; u: #$003F#$003F),
+    (e:'xcap'; u: #$003F),
+    (e:'xcirc'; u: #$003F),
+    (e:'xcup'; u: #$003F),
+    (e:'xdtri'; u: #$003F),
+    (e:'xfr'; u: #$003F#$003F),
+    (e:'xhArr'; u: #$003F),
+    (e:'xharr'; u: #$003F),
+    (e:'xi'; u: #$003F),
+    (e:'xlArr'; u: #$003F),
+    (e:'xlarr'; u: #$003F),
+    (e:'xmap'; u: #$003F),
+    (e:'xnis'; u: #$003F),
+    (e:'xodot'; u: #$003F),
+    (e:'xopf'; u: #$003F#$003F),
+    (e:'xoplus'; u: #$003F),
+    (e:'xotime'; u: #$003F),
+    (e:'xrArr'; u: #$003F),
+    (e:'xrarr'; u: #$003F),
+    (e:'xscr'; u: #$003F#$003F),
+    (e:'xsqcup'; u: #$003F),
+    (e:'xuplus'; u: #$003F),
+    (e:'xutri'; u: #$003F),
+    (e:'xvee'; u: #$003F),
+    (e:'xwedge'; u: #$003F),
+    (e:'yacute'; u: #$003F),
+    (e:'yacy'; u: #$003F),
+    (e:'ycirc'; u: #$003F),
+    (e:'ycy'; u: #$003F),
+    (e:'yen'; u: #$003F),
+    (e:'yfr'; u: #$003F#$003F),
+    (e:'yicy'; u: #$003F),
+    (e:'yopf'; u: #$003F#$003F),
+    (e:'yscr'; u: #$003F#$003F),
+    (e:'yucy'; u: #$003F),
+    (e:'yuml'; u: #$003F),
+    (e:'zacute'; u: #$003F),
+    (e:'zcaron'; u: #$003F),
+    (e:'zcy'; u: #$003F),
+    (e:'zdot'; u: #$003F),
+    (e:'zeetrf'; u: #$003F),
+    (e:'zeta'; u: #$003F),
+    (e:'zfr'; u: #$003F#$003F),
+    (e:'zhcy'; u: #$003F),
+    (e:'zigrarr'; u: #$003F),
+    (e:'zopf'; u: #$003F#$003F),
+    (e:'zscr'; u: #$003F#$003F),
+    (e:'zwj'; u: #$003F),
+    (e:'zwnj'; u: #$003F)
+);
+
+implementation
+
+end.

+ 773 - 0
packages/fcl-md/src/markdown.htmlrender.pas

@@ -0,0 +1,773 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown HTML 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.HtmlRender;
+
+{$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
+  { TMarkDownHTMLRenderer }
+  THTMLOption = (hoEnvelope,hoHead);
+  THTMLOptions = set of THTMLOption;
+
+  TMarkDownHTMLRenderer = class(TMarkDownRenderer)
+  private
+    FBuilder: TStringBuilder;
+    FHead: TStrings;
+    FHTML: String;
+    FOptions: THTMLOptions;
+    FTitle: 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 RenderHTML(aDocument : TMarkDownDocument) : string;
+    Property HTML : String Read FHTML;
+  published
+    Property Options : THTMLOptions Read FOptions Write FOptions;
+    property Title : String Read FTitle Write FTitle;
+    property Head : TStrings Read FHead Write SetHead;
+  end;
+
+  { THTMLMarkDownBlockRenderer }
+
+  THTMLMarkDownBlockRenderer = Class (TMarkDownBlockRenderer)
+  Private
+    function GetHTMLRenderer: TMarkDownHTMLRenderer;
+  protected
+    procedure Append(const S : String); inline;
+    procedure AppendNl(const S : String = ''); inline;
+    function HasOption(aOption : THTMLOption) : Boolean;
+  public
+    property HTMLRenderer : TMarkDownHTMLRenderer Read GetHTMLRenderer;
+  end;
+  THTMLMarkDownBlockRendererClass = class of THTMLMarkDownBlockRenderer;
+  { THTMLMarkDownTextRenderer }
+
+  THTMLMarkDownTextRenderer = class(TMarkDownTextRenderer)
+  Private
+    FStyleStack: Array of TNodeStyle;
+    FStyleStackLen : Integer;
+    FLastStyles : TNodeStyles;
+    FKeys : Array of String;
+    FKeyCount : integer;
+    procedure DoKey(aItem: AnsiString; const aKey: AnsiString; var aContinue: Boolean);
+    procedure EmitStyleDiff(aStyles: TNodeStyles);
+    function GetHTMLRenderer: TMarkDownHTMLRenderer;
+    function GetNodeTag(aElement: TMarkDownTextNode): string;
+    function MustCloseNode(aElement: TMarkDownTextNode): boolean;
+  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;
+  Public
+    procedure BeginBlock; override;
+    procedure EndBlock; override;
+    property HTMLRenderer : TMarkDownHTMLRenderer Read GetHTMLRenderer;
+    function renderAttrs(aElement: TMarkDownTextNode): AnsiString;
+  end;
+  THTMLMarkDownTextRendererClass = class of THTMLMarkDownTextRenderer;
+
+  { THTMLParagraphBlockRenderer }
+
+  THTMLParagraphBlockRenderer = class (THTMLMarkDownBlockRenderer)
+  protected
+    procedure DoRender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { THTMLMarkDownQuoteBlockRenderer }
+
+  THTMLMarkDownQuoteBlockRenderer = class(THTMLMarkDownBlockRenderer)
+  protected
+    procedure dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { THTMLMarkDownTextBlockRenderer }
+
+  THTMLMarkDownTextBlockRenderer = class(THTMLMarkDownBlockRenderer)
+  protected
+    procedure DoRender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { THTMLMarkDownListBlockRenderer }
+
+  THTMLMarkDownListBlockRenderer = class(THTMLMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { THTMLMarkDownListItemBlockRenderer }
+
+  THTMLMarkDownListItemBlockRenderer = class(THTMLMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { THTMLMarkDownCodeBlockRenderer }
+
+  THTMLMarkDownCodeBlockRenderer = class(THTMLMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { THTMLMarkDownHeadingBlockRenderer }
+
+  THTMLMarkDownHeadingBlockRenderer = class(THTMLMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { THTMLMarkDownThematicBreakBlockRenderer }
+
+  THTMLMarkDownThematicBreakBlockRenderer = class(THTMLMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { THTMLMarkDownTableBlockRenderer }
+
+  THTMLMarkDownTableBlockRenderer = class(THTMLMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { TMarkDownTableRowBlockRenderer }
+
+  THTMLMarkDownTableRowBlockRenderer = class(THTMLMarkDownBlockRenderer)
+  protected
+    procedure Dorender(aElement : TMarkDownBlock); override;
+  public
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+  { THTMLMarkDownDocumentRenderer }
+
+  THTMLMarkDownDocumentRenderer = class(THTMLMarkDownBlockRenderer)
+  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;
+
+{ TMarkDownBlockRenderer }
+
+function THTMLMarkDownBlockRenderer.GetHTMLRenderer: TMarkDownHTMLRenderer;
+begin
+  if Renderer is TMarkDownHTMLRenderer then
+    Result:=TMarkDownHTMLRenderer(Renderer)
+  else
+    Result:=Nil;
+end;
+
+procedure THTMLMarkDownBlockRenderer.Append(const S: String);
+begin
+  HTMLRenderer.Append(S);
+end;
+
+procedure THTMLMarkDownBlockRenderer.AppendNl(const S: String);
+begin
+  HTMLRenderer.AppendNL(S);
+end;
+
+function THTMLMarkDownBlockRenderer.HasOption(aOption: THTMLOption): Boolean;
+begin
+  Result:=(Self.Renderer is TMarkDownHTMLRenderer);
+  if Result then
+    Result:=aOption in TMarkDownHTMLRenderer(Renderer).Options;
+end;
+
+
+{ TMarkDownHTMLRenderer }
+
+procedure TMarkDownHTMLRenderer.SetHead(const aValue: TStrings);
+begin
+  if FHead=aValue then Exit;
+  FHead:=aValue;
+end;
+
+procedure TMarkDownHTMLRenderer.Append(const aContent: String);
+begin
+  FBuilder.Append(aContent);
+end;
+
+procedure TMarkDownHTMLRenderer.AppendNL(const aContent: String);
+begin
+  if aContent<>'' then
+    FBuilder.Append(aContent);
+  FBuilder.Append(sLineBreak);
+end;
+
+constructor TMarkDownHTMLRenderer.Create(aOwner: TComponent);
+begin
+  inherited Create(aOwner);
+  FHead:=TStringList.Create;
+end;
+
+destructor TMarkDownHTMLRenderer.destroy;
+begin
+  FreeAndNil(FHead);
+  inherited destroy;
+end;
+
+procedure TMarkDownHTMLRenderer.RenderDocument(aDocument: TMarkDownDocument);
+begin
+  FBuilder:=TStringBuilder.Create;
+  try
+    RenderBlock(aDocument);
+    FHTML:=FBuilder.ToString;
+  finally
+    FreeAndNil(FBuilder);
+  end;
+end;
+
+procedure TMarkDownHTMLRenderer.RenderDocument(aDocument: TMarkDownDocument; aDest: TStrings);
+begin
+  aDest.Text:=RenderHTML(aDocument);
+end;
+
+procedure TMarkDownHTMLRenderer.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 TMarkDownHTMLRenderer.RenderHTML(aDocument: TMarkDownDocument): string;
+begin
+  RenderDocument(aDocument);
+  Result:=FHTML;
+  FHTML:='';
+end;
+
+
+procedure THTMLMarkDownTextRenderer.Append(const S: String);
+begin
+  HTMLRenderer.Append(S);
+end;
+
+function THTMLMarkDownTextRenderer.MustCloseNode(aElement: TMarkDownTextNode) : boolean;
+
+begin
+  Result:=aElement.kind<>nkImg;
+end;
+
+const
+  StyleNames : Array[TNodeStyle] of string = ('b','i','del');
+
+procedure THTMLMarkDownTextRenderer.PushStyle(aStyle: TNodeStyle);
+
+begin
+  HTMLRenderer.Append('<'+styleNames[aStyle]+'>');
+  if FStyleStackLen=Length(FStyleStack) then
+    SetLength(FStyleStack,FStyleStackLen+3);
+  FStyleStack[FStyleStackLen]:=aStyle;
+  Inc(FStyleStackLen);
+end;
+
+function THTMLMarkDownTextRenderer.Popstyles(aStyle: TNodeStyles) : TNodeStyle;
+
+begin
+  if (FStyleStackLen>0) and (FStyleStack[FStyleStackLen-1] in aStyle) then
+    begin
+    Result:=FStyleStack[FStyleStackLen-1];
+    HTMLRenderer.Append('</'+StyleNames[Result]+'>');
+    Dec(FStyleStackLen);
+    end;
+end;
+
+procedure THTMLMarkDownTextRenderer.PopStyle(aStyle: TNodeStyle);
+begin
+  if (FStyleStackLen>0) and (FStyleStack[FStyleStackLen-1]=aStyle) then
+    begin
+    HTMLRenderer.Append('</'+styleNames[aStyle]+'>');
+    Dec(FStyleStackLen);
+    end;
+end;
+
+function THTMLMarkDownTextRenderer.GetNodeTag(aElement: TMarkDownTextNode) : string;
+begin
+  case aElement.Kind of
+    nkCode: Result:='code';
+    nkImg : Result:='img';
+    nkURI,nkEmail : Result:='a'
+  end;
+end;
+
+function THTMLMarkDownTextRenderer.GetHTMLRenderer: TMarkDownHTMLRenderer;
+begin
+  if Renderer is TMarkDownHTMLRenderer then
+    Result:=TMarkDownHTMLRenderer(Renderer)
+  else
+    Result:=Nil;
+end;
+
+procedure THTMLMarkDownTextRenderer.DoKey(aItem: AnsiString; const aKey: Ansistring; var aContinue: Boolean);
+begin
+  aContinue:=True;
+  FKeys[FKeyCount]:=aKey;
+  inc(FKeyCount);
+end;
+
+procedure THTMLMarkDownTextRenderer.EmitStyleDiff(aStyles : TNodeStyles);
+
+var
+  lRemove : TNodeStyles;
+  lAdd : TNodeStyles;
+  S : TNodeStyle;
+
+begin
+  lRemove:=[];
+  lAdd:=[];
+  For S in TNodeStyle do
+    begin
+    if (S in FLastStyles) and Not (S in aStyles) then
+      Include(lRemove,S);
+    if (S in aStyles) and Not (S in FLastStyles) then
+      Include(lAdd,S);
+    end;
+  While lRemove<>[] do
+    begin
+    S:=PopStyles(lRemove);
+    Exclude(lRemove,S);
+    end;
+  For S in TNodeStyle do
+    if S in lAdd then
+      PushStyle(S);
+  FLastStyles:=aStyles;
+end;
+
+procedure THTMLMarkDownTextRenderer.DoRender(aElement: TMarkDownTextNode);
+var
+  lName : string;
+begin
+  EmitStyleDiff(aElement.Styles);
+  if aElement.Kind<>nkText then
+    begin
+    lName:=GetNodeTag(aElement);
+    Append('<');
+    Append(lName);
+    Append(renderAttrs(aElement));
+    Append('>');
+    end;
+  if aElement.NodeText<>'' then
+    Append(aElement.NodeText);
+  if (lName<>'') and MustCloseNode(aElement) then
+    begin
+    Append('</');
+    Append(lName);
+    Append('>');
+    end;
+  aElement.Active:=False;
+end;
+
+procedure THTMLMarkDownTextRenderer.BeginBlock;
+begin
+  inherited BeginBlock;
+  FStyleStackLen:=0;
+  FLastStyles:=[];
+end;
+
+procedure THTMLMarkDownTextRenderer.EndBlock;
+begin
+  While (FStyleStackLen>0) do
+    Popstyle(FStyleStack[FStyleStackLen-1]);
+  FLastStyles:=[];
+  inherited EndBlock;
+end;
+
+function THTMLMarkDownTextRenderer.renderAttrs(aElement: TMarkDownTextNode): AnsiString;
+
+  procedure addKey(aKey,aValue : String);
+  begin
+    Result:=Result+' '+aKey+'="'+aValue+'"';
+  end;
+
+var
+  lKey,lAttr : String;
+  lAttrs : THashTable;
+  lKeys : Array of string;
+begin
+  result := '';
+  if not Assigned(aElement.Attrs) then
+    exit;
+  lAttrs:=aElement.Attrs;
+  // First the known keys
+  lKeys:=['src','alt','href','title'];
+  for lKey in lKeys do
+    if lAttrs.TryGet(lKey,lAttr) then
+      AddKey(lKey,lAttr);
+  // Then the other keys
+  SetLength(FKeys,lAttrs.Count);
+  FKeyCount:=0;
+  lAttrs.Iterate(@DoKey);
+  for lKey in FKeys do
+    if IndexStr(lKey,['src','alt','href','title'])=-1 then
+      AddKey(lKey,lAttrs[lKey]);
+end;
+
+procedure THTMLParagraphBlockRenderer.DoRender(aElement: TMarkDownBlock);
+var
+  lNode : TMarkDownParagraphBlock absolute aElement;
+  c : TMarkDownBlock;
+  first : boolean;
+begin
+  if lNode.header=0 then
+    Append('<p>')
+  else
+    Append('<h'+IntToStr(lNode.Header)+'>');
+  first := true;
+  for c in lNode.Blocks do
+    begin
+    if first then
+      first := false
+    else
+      AppendNl;
+   Renderer.RenderChildren(lNode);
+    end;
+  if lNode.header=0 then
+    Append('</p>')
+  else
+    Append('</h'+IntToStr(lNode.Header)+'>');
+  AppendNl;
+end;
+
+class function THTMLParagraphBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownParagraphBlock;
+end;
+
+class function THTMLMarkDownTextBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownTextBlock;
+end;
+
+procedure THTMLMarkDownTextBlockRenderer.DoRender(aElement: TMarkDownBlock);
+var
+  lNode : TMarkDownTextBlock absolute aElement;
+begin
+  if assigned(lNode) and assigned(lNode.Nodes) then
+    Renderer.RenderTextNodes(lNode.Nodes);
+end;
+
+procedure THTMLMarkDownQuoteBlockRenderer.dorender(aElement: TMarkDownBlock);
+var
+  lNode : TMarkdownQuoteBlock absolute aElement;
+
+begin
+  AppendNl('<blockquote>');
+  Renderer.RenderChildren(lNode);
+  AppendNl('</blockquote>');
+end;
+
+class function THTMLMarkDownQuoteBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownQuoteBlock;
+end;
+
+procedure THTMLMarkDownListBlockRenderer.Dorender(aElement : TMarkDownBlock);
+
+var
+  lNode : TMarkDownListBlock absolute aElement;
+
+begin
+  if not lNode.Ordered then
+    AppendNl('<ul>')
+  else if lNode.Start=1 then
+    AppendNL('<ol>')
+  else
+    AppendNl('<ol start="'+IntToStr(lNode.Start)+'">');
+  Renderer.RenderChildren(lNode);
+  if lNode.Ordered then
+    AppendNl('</ol>')
+  else
+    AppendNl('</ul>');
+end;
+
+class function THTMLMarkDownListBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownListBlock;
+end;
+
+
+procedure THTMLMarkDownListItemBlockRenderer.Dorender(aElement : TMarkDownBlock);
+var
+  lItemBlock : TMarkDownListItemBlock absolute aElement;
+  lBlock : TMarkDownBlock;
+  lPar : TMarkDownParagraphBlock absolute lBlock;
+  lCount : Integer;
+
+  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('<li>');
+  lCount:=0;
+  For lBlock in lItemBlock.Blocks do
+    if IsPlainBlock(lBlock) then
+      HTMLRenderer.RenderChildren(lPar,True)
+    else
+      begin
+      if lCount=0 then
+        AppendNl;
+      Inc(lCount);
+      Renderer.RenderBlock(lBlock);
+      end;
+  AppendNl('</li>');
+end;
+
+class function THTMLMarkDownListItemBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownListItemBlock;
+end;
+
+procedure THTMLMarkDownCodeBlockRenderer.Dorender(aElement : TMarkDownBlock);
+var
+  lNode : TMarkDownCodeBlock absolute aElement;
+  lBlock : TMarkDownBlock;
+  lLang : string;
+begin
+  lLang:=lNode.Lang;
+  if lLang<> '' then
+    Append('<pre><code class="language-'+lLang+'">')
+  else
+    Append('<pre><code>');
+  for lBlock in LNode.Blocks do
+    begin
+    Renderer.RenderCodeBlock(LBlock,lLang);
+    AppendNl;
+    end;
+  Append('</code></pre>');
+  AppendNl;
+end;
+
+class function THTMLMarkDownCodeBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownCodeBlock;
+end;
+
+procedure THTMLMarkDownThematicBreakBlockRenderer.Dorender(aElement : TMarkDownBlock);
+
+begin
+  if Not Assigned(aElement) then
+    exit;
+  Append('<hr />');
+  AppendNl;
+end;
+
+class function THTMLMarkDownThematicBreakBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownThematicBreakBlock;
+end;
+
+{ TMarkDownTableBlock }
+
+procedure THTMLMarkDownTableBlockRenderer.Dorender(aElement: TMarkDownBlock);
+var
+  lNode : TMarkDownTableBlock absolute aElement;
+  i : integer;
+begin
+  AppendNl('<table>');
+  AppendNl('<thead>');
+  Renderer.RenderBlock(lNode.blocks[0]);
+  AppendNl('</thead>');
+  if lNode.blocks.Count > 1 then
+  begin
+    AppendNl('<tbody>');
+    for i := 1 to lNode.blocks.Count -1  do
+      Renderer.RenderBlock(lnode.blocks[i]);
+    AppendNl('</tbody>');
+  end;
+  AppendNl('</table>');
+end;
+
+class function THTMLMarkDownTableBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownTableBlock;
+end;
+
+{ THTMLMarkDownDocumentRenderer }
+
+procedure THTMLMarkDownDocumentRenderer.Dorender(aElement: TMarkDownBlock);
+var
+  H : String;
+begin
+  if HasOption(hoEnvelope) then
+    begin
+    AppendNL('<!DOCTYPE html>');
+    AppendNL('<html>');
+    if HasOption(hoHead) then
+      begin
+      AppendNL('<head>');
+      if HTMLRenderer.Title<>'' then
+        begin
+        Append('<title>');
+        Append(HTMLRenderer.Title);
+        AppendNL('</title>');
+        end;
+      for H in HTMLRenderer.Head do
+        AppendNL(H);
+      AppendNL('</head>');
+      end;
+    AppendNL('<body>');
+    end;
+  Renderer.RenderChildren(aElement as TMarkDownDocument);
+  if HasOption(hoEnvelope) then
+    begin
+    AppendNL('</body>');
+    AppendNL('</html>');
+    end;
+end;
+
+class function THTMLMarkDownDocumentRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownDocument
+end;
+
+{ TMarkDownTableRowBlock }
+
+procedure THTMLMarkDownTableRowBlockRenderer.Dorender(aElement : TMarkDownBlock);
+const
+  CellTypes : Array[Boolean] of string = ('td','th'); //
+var
+  lNode : TMarkDownTableRowBlock absolute aElement;
+  lFirst : boolean;
+  i,lCount : integer;
+  lType,lAttr : String;
+  lAlign: TCellAlign;
+begin
+  lFirst:=(lNode.parent as TMarkDownContainerBlock).blocks.First = self;
+  lCount:=length((lNode.parent as TMarkDownTableBlock).Columns);
+  lType:=CellTypes[lFirst];
+  AppendNl('<tr>');
+  for i:=0 to lCount-1 do
+    begin
+    lAlign:=(lNode.parent as TMarkDownTableBlock).Columns[i];
+    case lAlign of
+      caLeft   : lAttr:='';
+      caCenter : lAttr:=' align="center"';
+      caRight  : lAttr:=' align="right"';
+    end;
+    Append('<'+lType+lAttr+'>');
+    if i<lNode.blocks.Count then
+      Renderer.RenderBlock(lNode.blocks[i]);
+    AppendNl('</'+lType+'>');
+    end;
+  AppendNl('</tr>');
+end;
+
+class function THTMLMarkDownTableRowBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownTableRowBlock;
+end;
+
+procedure THTMLMarkDownHeadingBlockRenderer.Dorender(aElement : TMarkDownBlock);
+
+var
+  lNode : TMarkDownHeadingBlock absolute aElement;
+begin
+  Append('<h'+inttostr(Lnode.Level)+'>');
+  Renderer.RenderChildren(lNode);
+  Append('</h'+inttostr(lNode.Level)+'>');
+  AppendNl;
+end;
+
+class function THTMLMarkDownHeadingBlockRenderer.BlockClass: TMarkDownBlockClass;
+begin
+  Result:=TMarkDownHeadingBlock;
+end;
+
+initialization
+  THTMLMarkDownHeadingBlockRenderer.RegisterRenderer(TMarkDownHTMLRenderer);
+  THTMLParagraphBlockRenderer.RegisterRenderer(TMarkdownHTMLRenderer);
+  THTMLMarkDownQuoteBlockRenderer.RegisterRenderer(TMarkdownHTMLRenderer);
+  THTMLMarkDownTextBlockRenderer.RegisterRenderer(TMarkdownHTMLRenderer);
+  THTMLMarkDownListBlockRenderer.RegisterRenderer(TMarkdownHTMLRenderer);
+  THTMLMarkDownListItemBlockRenderer.RegisterRenderer(TMarkdownHTMLRenderer);
+  THTMLMarkDownCodeBlockRenderer.RegisterRenderer(TMarkdownHTMLRenderer);
+  THTMLMarkDownHeadingBlockRenderer.RegisterRenderer(TMarkdownHTMLRenderer);
+  THTMLMarkDownThematicBreakBlockRenderer.RegisterRenderer(TMarkdownHTMLRenderer);
+  THTMLMarkDownTableBlockRenderer.RegisterRenderer(TMarkdownHTMLRenderer);
+  THTMLMarkDownTableRowBlockRenderer.RegisterRenderer(TMarkdownHTMLRenderer);
+  THTMLMarkDownDocumentRenderer.RegisterRenderer(TMarkdownHTMLRenderer);
+  THTMLMarkDownTextRenderer.RegisterRenderer(TMarkdownHTMLRenderer);
+end.
+

+ 984 - 0
packages/fcl-md/src/markdown.inlinetext.pas

@@ -0,0 +1,984 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown inline text handler.
+
+    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.
+
+ **********************************************************************}
+
+{
+  Some ideas for the inline text parsing were gleaned from the parser at
+  https://github.com/grahamegrieve/delphi-markdown
+}
+
+unit MarkDown.InlineText;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+{$IFDEF FPC_DOTTEDUNITS}
+  System.Classes, System.SysUtils, System.Contnrs,
+{$ELSE}
+  Classes, SysUtils, Contnrs,
+{$ENDIF}  
+  MarkDown.Scanner,
+  MarkDown.Elements,
+  MarkDown.Utils;
+
+Type
+  TMarkDownDelimiterMode = (dmOpen,dmClose);
+  TMarkDownDelimiterModes = Set of TMarkDownDelimiterMode;
+
+  { TMarkDownDelimiter }
+
+  TMarkDownDelimiter = class
+  private
+    FModes: TMarkDownDelimiterModes;
+    FDelimiter: String;
+    FActive: boolean;
+    FNode: TMarkDownTextNode;
+  public
+    constructor Create(aNode: TMarkDownTextNode; const aDelimiter: String; aModes: TMarkDownDelimiterModes);
+    procedure RemoveLast(aCount : integer);
+    function CanClose(aDelim: TMarkDownDelimiter) : boolean;
+    function Opens : Boolean;
+    function Closes : Boolean;
+    function OpensCloses : Boolean;
+    property Node : TMarkDownTextNode read FNode;
+    property Delimiter : String read FDelimiter write FDelimiter;
+    property Active : Boolean read FActive write FActive;
+    property Modes : TMarkDownDelimiterModes read FModes write FModes;
+    function IsEmph : Boolean;
+  end;
+  TMarkDownDelimiterList = class (specialize TGFPObjectList<TMarkDownDelimiter>);
+
+  { TInlineTextProcessor }
+
+  TInlineTextProcessor = class
+  Private
+    FGFMExtensions : Boolean;
+    FScanner : TMarkDownTextScanner;
+    FNodes: TMarkDownTextNodeList;
+    FWhiteSpaceMode: TWhitespaceMode;
+    FEntities : TFPStringHashTable;
+    FStack : TMarkDownDelimiterList;
+  protected
+    // Reading
+    function ReadInlineBracketedLink(aBuilder: TStringBuilder): Boolean;
+    function ReadInlineNormalLink(aBuilder: TStringBuilder): Boolean;
+    function ReadLinkTitle(aBuilder: TStringBuilder): Boolean;
+    function PeekEmailAddress(out len: integer): boolean; virtual;
+    procedure AddTextTillNext(const aTerminal: String); virtual;
+    // Handle various constructs
+    procedure HandleEmphasis(aTerminator: TMarkDownDelimiter); virtual;
+    function  HandleInlineLink(aDelim: TMarkDownDelimiter): boolean; virtual;
+    procedure HandleBackTick; virtual;
+    procedure HandleDelimiter(aAllowMulti: boolean); virtual;
+    procedure HandleEntity; virtual;
+    function  HandleEntityInner: String; virtual;
+    procedure HandleTilde; virtual;
+    procedure HandleTextEscape; virtual;
+    procedure HandleTextCore; virtual;
+    procedure HandleAutoLink; virtual;
+    procedure HandleCloseDelimiter(); virtual;
+    procedure HandleGFMExtensions; virtual;
+    procedure HandleGFMLinkEmail(aLength: integer); virtual;
+    procedure HandleGFMLinkURL(aStartChars : integer; const aProtocol : string = ''); virtual;
+  public
+    constructor Create(aLexer : TMarkDownTextScanner; aNodes: TMarkDownTextNodeList; aEntities : TFPStringHashTable; awsMode : TWhitespaceMode);
+    destructor Destroy; override;
+    function process(aCloseLast : boolean = false): TMarkDownTextNodeList;
+    procedure DumpNodes;
+    property Scanner : TMarkDownTextScanner read FScanner;
+    property Nodes : TMarkDownTextNodeList Read FNodes;
+    property WhitespaceMode : TWhitespaceMode read FWhiteSpaceMode;
+    Property GFMExtensions : Boolean Read FGFMExtensions Write FGFMExtensions;
+  end;
+  TInlineTextProcessorClass = class of TInlineTextProcessor;
+
+implementation
+
+{$IFDEF FPC_DOTTEDUNITS}
+uses System.StrUtils, System.Math;
+{$ELSE}
+uses StrUtils, Math;
+{$ENDIF}
+
+const
+  dmOpenClose = [dmOpen,dmClose];
+
+{ TMarkDownDelimiter }
+
+constructor TMarkDownDelimiter.Create(aNode: TMarkDownTextNode; const aDelimiter : String; aModes: TMarkDownDelimiterModes);
+begin
+  inherited create;
+  FNode:=aNode;
+  FModes:=aModes;
+  FDelimiter:=aDelimiter;
+  FActive:=true;
+end;
+
+procedure TInlineTextProcessor.DumpNodes;
+
+var
+  I : Integer;
+
+begin
+  Writeln('Current Nodes (',FNodes.Count,'):');
+  for I:=0 to FNodes.Count-1 do
+    With FNodes[i] do
+      Writeln('Node ',I,'[',Kind,']: ',NodeText);
+  Writeln;
+end;
+
+
+procedure TMarkDownDelimiter.RemoveLast(aCount: integer);
+begin
+  SetLength(FDelimiter,Length(FDelimiter)-aCount);
+end;
+
+function TMarkDownDelimiter.CanClose(aDelim: TMarkDownDelimiter): boolean;
+begin
+  Result:=(Delimiter[1]=aDelim.Delimiter[1]) and aDelim.Opens;
+end;
+
+function TMarkDownDelimiter.Closes: Boolean;
+begin
+  Result:=dmClose in FModes;
+end;
+
+function TMarkDownDelimiter.OpensCloses: Boolean;
+begin
+  Result:=(FModes=dmOpenClose);
+end;
+
+function TMarkDownDelimiter.IsEmph: Boolean;
+
+begin
+  Result:=(Delimiter<>'[') and (Delimiter<>'![');
+end;
+
+
+function TMarkDownDelimiter.Opens: Boolean;
+
+begin
+  Result:=dmOpen in FModes;
+end;
+
+{ TInlineTextProcessor }
+
+constructor TInlineTextProcessor.Create(aLexer: TMarkDownTextScanner; aNodes: TMarkDownTextNodeList; aEntities: TFPStringHashTable; awsMode: TWhitespaceMode);
+
+begin
+  FScanner:=aLexer;
+  FNodes:=aNodes;
+  FWhiteSpaceMode:=awsMode;
+  FEntities:=aEntities;
+  FStack:=TMarkDownDelimiterList.Create;
+end;
+
+destructor TInlineTextProcessor.Destroy;
+begin
+  FreeAndNil(FStack);
+  inherited destroy;
+end;
+
+procedure TInlineTextProcessor.HandleTextEscape;
+
+var
+  C : Char;
+
+begin
+  Scanner.Bookmark;
+  Scanner.NextChar();
+  C:=Scanner.Peek;
+  if MustEscape(C) then
+    Nodes.addText(Scanner.location,htmlEscape(Scanner.NextChar()))
+  else if C=#10 then
+    Nodes.addTextNode(Scanner.location,nkLineBreak,'')
+  else
+    begin
+    Scanner.GotoBookmark;
+    Nodes.addText(Scanner.location,Scanner.NextChar());
+    end;
+end;
+
+
+procedure TInlineTextProcessor.HandleAutoLink;
+
+var
+  lText : String;
+  lNode : TMarkDownTextNode;
+  lAdd : boolean;
+  lNodeKind : TTextNodeKind;
+  lURL : String;
+
+begin
+  Scanner.BookMark;
+  Scanner.NextChar();
+  lText:=Scanner.PeekUntil(['>']);
+  lAdd:=false;
+  if (WhiteSpaceMode<>wsLeave) then
+    begin
+    lAdd:=isAbsoluteURI(lText);
+    if lAdd then
+      begin
+      lNodeKind:=nkURI;
+      lURL:=urlEscape(lText);
+      end
+    else
+      begin
+      lAdd:=IsValidEmail(lText);
+      if lAdd then
+        begin
+        lNodeKind:=nkEmail;
+        lURL:='mailto:'+urlEscape(lText);
+        end
+      end;
+    if lAdd then
+      begin
+      lNode:=Nodes.addTextNode(Scanner.location,lNodeKind,lText);
+      lNode.attrs.Add('href',lURL);
+      Scanner.NextChars(Length(lText));
+      Scanner.NextChar();
+      end;
+    end;
+  if lAdd then
+    Exit;
+  Scanner.GotoBookmark;
+  Nodes.addText(Scanner.location,'&lt;');
+  Scanner.NextChar();
+end;
+
+procedure TInlineTextProcessor.HandleEntity;
+begin
+  if WhiteSpaceMode<>wsLeave then
+    Nodes.addText(Scanner.location,htmlEscape(HandleEntityInner()))
+  else
+    begin
+    Nodes.addText(Scanner.location,'&amp;');
+    Scanner.NextChar();
+    end
+end;
+
+function TInlineTextProcessor.HandleEntityInner: String;
+
+var
+  lEntity : String;
+  ch : UnicodeChar;
+  I,lLen : integer;
+
+begin
+  Scanner.NextChar();
+  lEntity:=Scanner.PeekUntil([';']);
+  lLen:=Length(lEntity);
+  Result:=FEntities.Items[lEntity];
+  if Result<>'' then
+    begin
+    Scanner.NextChars(lLen+1);
+    Exit;
+    end;
+  Result:='&';
+  // If not a unicode char, just return the &
+  if (lLen<0) or (lLen>9) or (lEntity[1]<>'#') then
+    exit;
+  // Unicode char.
+  System.Delete(lEntity,1,1);
+  if TryStrToInt(lEntity,I) then
+    begin
+    if (i=0) or (i>65535) then
+      ch:=#$FFFD
+    else
+      ch:=UnicodeChar(I);
+    Scanner.NextChars(lLen+1);
+    Result:=Utf8Encode(ch);
+    end;
+end;
+
+procedure TInlineTextProcessor.HandleBackTick;
+var
+  lBackTick : String;
+  lLen : integer;
+
+begin
+  lBackTick:=Scanner.PeekRun(false);
+  lLen:=length(lBackTick);
+  if not Scanner.FindMatchingOccurrence(lBackTick) then
+    begin
+    Nodes.addText(Scanner.location,Scanner.NextChars(lLen));
+    Exit;
+    end;
+  Nodes.addTextNode(Scanner.location,nkCode,'',false);
+  AddTextTillNext(lBackTick);
+end;
+
+procedure TInlineTextProcessor.HandleTilde;
+var
+  lTilde : String;
+  lLen : Integer;
+  ldelete : boolean;
+  lNode : TMarkDownTextNode;
+
+begin
+  lTilde:=Scanner.PeekRun(false);
+  lLen:=Length(lTilde);
+  lDelete:=FGFMExtensions and (lLen=2) and Scanner.FindMatchingOccurrence(lTilde,#10);
+  if not lDelete then
+    begin
+    Nodes.addText(Scanner.location,Scanner.NextChars(lLen));
+    Exit;
+    end;
+  lNode:=Nodes.addTextNode(Scanner.location,nkText,'',False);
+  lNode.AddStyle(nsDelete);
+  AddTextTillNext(lTilde);
+end;
+
+procedure TInlineTextProcessor.AddTextTillNext(const aTerminal : String);
+var
+  lWhitespace,lFirstWhitespace : boolean;
+  C : char;
+  lLen : integer;
+begin
+  lLen:=Length(aTerminal);
+  Scanner.NextChars(lLen);
+  lWhitespace:=false;
+  lFirstWhitespace:=true;
+  while Scanner.PeekRun(True)<>aTerminal do
+    begin
+    C:=Scanner.Peek;
+    if isWhitespaceChar(C) then
+      begin
+      lWhitespace:=true;
+      Scanner.NextChar;
+      end
+    else
+      begin
+      if lWhitespace and not lFirstWhitespace then
+        Nodes.addText(Scanner.location,' ');
+      lFirstWhitespace:=false;
+      lWhitespace:=false;
+      Nodes.addText(Scanner.location,htmlEscape(Scanner.NextChar));
+      end;
+    end;
+  Scanner.NextChars(lLen);
+  Nodes.lastNode.Active:=False;
+end;
+
+procedure TInlineTextProcessor.HandleDelimiter(aAllowMulti : boolean);
+
+  function IsWhiteSpace(aChar : Char) : boolean;
+  // Handle end of line in addition to normal whitespace
+  const
+    EOL = [#0,#10];
+  begin
+    Result:=CharInSet(aChar,EOL) or Markdown.Utils.IsWhiteSpace(aChar);
+  end;
+
+  function isPunctuation(aChar : Char) : boolean;
+  // Handle special chars in addition to Unicode punctuation
+  const
+    SpecialPunctuation = ['!','"','#','$','%','&','''','(',')','*','+',',','-','.','/',':',';','<','=','>','?','@','[','\',']','^','_','`','{','|','}','~'];
+  begin
+    Result:=CharInSet(aChar,SpecialPunctuation) or isUnicodePunctuation(aChar);
+  end;
+
+  function isLeftFlanking(aPrevious,aNext : char) : boolean;
+
+  begin
+   // (1) not followed by Unicode whitespace.
+    Result:=not isWhiteSpace(aNext);
+    if not Result then
+      Exit;
+    // (2a) not followed by a punctuation character.
+    // or
+    // (2b) preceded by Unicode whitespace or a punctuation character.
+    Result:=(not isPunctuation(aNext)) // 2a
+             or isWhiteSpace(aPrevious)   // 2b
+             or isPunctuation(aPrevious); // 2b
+  end;
+
+  function isRightFlanking(aPrevious,aNext : char) : boolean;
+
+  begin
+    // (1) not preceded by Unicode whitespace.
+    Result:=Not isWhiteSpace(aPrevious);
+    if not Result then
+      exit;
+    // (2a) not preceded by a Unicode punctuation character.
+    // or
+    // (2b) preceded by a Unicode punctuation character and followed by Unicode whitespace or a Unicode punctuation character.
+    Result:=(not isPunctuation(aPrevious)) // 2a
+            or isWhiteSpace(aNext) or isPunctuation(aNext); // 2b. No need to check unicode punctuation, it's true at this point
+  end;
+
+var
+  cPrevious,cNext : Char;
+  lDelim : String;
+  lLeft,lRight : boolean;
+  lPos : TPosition;
+  lModes : TMarkDownDelimiterModes;
+  lNode : TMarkDownTextNode;
+
+begin
+  if aAllowMulti then
+    lDelim:=Scanner.PeekRun(false)
+  else
+    lDelim:=Scanner.PeekLen(1+Ord(Scanner.Peek='!'));
+  cPrevious:=Scanner.PeekPrevious;
+  lPos:=Scanner.location;
+  // have pos, skip delimiter
+  Scanner.NextChars(Length(lDelim));
+  cNext:=Scanner.Peek;
+  lLeft:=isLeftFlanking(cPrevious,cNext);
+  lRight:=isRightFLanking(cPrevious,cNext);
+  if lLeft then
+    begin
+    if lRight then
+      lModes:=[dmOpen,dmClose]
+    else
+      lModes:=[dmOpen]
+    end
+  else if lRight then
+    lModes:=[dmClose]
+  else
+    lModes:=[];
+  lNode:=Nodes.addTextNode(lPos,nkText,'',False);
+  FStack.Add(TMarkDownDelimiter.Create(lNode,lDelim,lModes));
+end;
+
+function TInlineTextProcessor.ReadInlineBracketedLink(aBuilder : TStringBuilder) : Boolean;
+
+begin
+  Result:=False;
+  Scanner.NextChar;
+  while not Scanner.EOF and (Scanner.Peek <> '>') do
+    begin
+    if (Scanner.Peek='\') and MustEscape(Scanner.PeekNext) then
+      begin
+      Scanner.NextChar;
+      if Not Scanner.EOF then
+        aBuilder.Append(Scanner.NextChar);
+      end
+    else if isWhitespaceChar(Scanner.Peek) then
+      begin
+      if FGFMExtensions and (Scanner.Peek=' ') then
+        begin
+        aBuilder.Append('%20');
+        Scanner.NextChar;
+        end
+      else
+        Exit;
+      end
+    else if Scanner.Peek='&' then
+      aBuilder.Append(HandleEntityInner)
+    else
+      aBuilder.Append(Scanner.NextChar);
+    end;
+  Result:=Not Scanner.EOF;
+end;
+
+function TInlineTextProcessor.ReadInlineNormalLink(aBuilder : TStringBuilder) : Boolean;
+
+var
+  lLevel : Integer;
+  C : Char;
+
+  function EndOfLink : boolean;
+  var
+    lNext : Char;
+  begin
+    lNext:=Scanner.Peek;
+    Result:=((lLevel=0) and (lNext=')')) or isWhitespace(lNext);
+  end;
+
+begin
+  Result:=False;
+  lLevel:=0;
+  while not Scanner.EOF and not EndOfLink  do
+    begin
+    C:=Scanner.Peek;
+    if (C='\') and MustEscape(Scanner.PeekNext) then
+      begin
+      Scanner.NextChar;
+      if Not Scanner.EOF then
+        aBuilder.Append(Scanner.NextChar);
+      end
+    else if isWhitespaceChar(C) then
+      Exit
+    else if (C='&') then
+      aBuilder.Append(HandleEntityInner())
+    else
+      begin
+      if C='(' then
+        inc(lLevel)
+      else if (C=')') then
+        dec(lLevel);
+      aBuilder.Append(Scanner.NextChar);
+      end;
+    end;
+  Result:=Not Scanner.EOF;
+end;
+
+function TInlineTextProcessor.ReadLinkTitle(aBuilder : TStringBuilder) : Boolean;
+
+Var
+  C,EOT : char;
+
+begin
+  Result:=False;
+  Scanner.skipWhitespace;
+  EOT:=Scanner.NextChar;
+  if not CharInSet(EOT,['"','''','(']) then
+    Exit;
+  if (EOT='(') then
+    EOT:=')';
+  while not Scanner.EOF and (Scanner.Peek<>EOT) do
+    begin
+    C:=Scanner.Peek;
+    Case C of
+    '\':
+       begin
+       Scanner.NextChar;
+       if Scanner.EOF then
+         Exit;
+       aBuilder.Append(htmlEscape(Scanner.NextChar));
+       end;
+    '&':
+       aBuilder.Append(htmlEscape(HandleEntityInner()));
+    #10:
+       aBuilder.Append(' ');
+    '"':
+      aBuilder.Append(htmlEscape(Scanner.NextChar));
+    else
+      aBuilder.Append(Scanner.NextChar);
+    end;
+    end;
+  Result:=Not Scanner.EOF;
+end;
+
+function TInlineTextProcessor.HandleInlineLink(aDelim : TMarkDownDelimiter): boolean;
+{
+  There are 4 cases we want to handle:
+  - inline link/image  [text](link title)
+  - reference link/image [text][ref]
+  - compact reference link/image [label][]
+  - shortcut reference link/image [label]
+}
+
+var
+  lURL : String;
+  lTitle : String;
+  lBuilder : TStringBuilder;
+  lDelim : TMarkDownDelimiter;
+
+begin
+  Result:=false;
+  lTitle:='';
+  lBuilder:=TStringBuilder.Create;
+  Scanner.BookMark;
+  try
+    Scanner.NextChar;
+    if Scanner.Peek<>'(' then
+      Exit;
+    Scanner.NextChar; // (
+    Scanner.skipWhitespace;
+    if Scanner.Peek='<' then
+      begin
+      if not ReadInlineBracketedlink(lBuilder) then
+        Exit;
+      Scanner.NextChar;
+      end
+    else if not ReadInlineNormalLink(lBuilder) then
+      Exit;
+    lURL:=URLEscape(lBuilder.ToString);
+    if Scanner.Peek <> ')' then
+      begin
+      lBuilder.clear;
+      if not ReadLinkTitle(lBuilder) then
+        Exit;
+      Scanner.NextChar;
+      lTitle:=lBuilder.toString;
+      end;
+    Scanner.skipWhiteSpace;
+    if Scanner.Peek <> ')' then
+      Exit;
+    Scanner.NextChar;
+    if aDelim.delimiter = '![' then
+      begin
+      aDelim.Node.Kind:=nkimg;
+      aDelim.Node.active:=false;
+      aDelim.Node.attrs.Add('src',URLEscape(lURL));
+      aDelim.Node.attrs.Add('alt',aDelim.node.NodeText);
+      end
+    else
+      begin
+      aDelim.Node.Kind:=nkUri;
+      aDelim.Node.attrs.Add('href',URLEscape(lURL));
+      aDelim.Node.active:=false;
+      for lDelim in FStack do
+        if lDelim = aDelim then
+          break
+        else if lDelim.delimiter='[' then
+          lDelim.active:=false;
+      end;
+    if lTitle<>'' then
+      aDelim.node.attrs.Add('title',lTitle);
+    HandleEmphasis(aDelim);
+    FStack.remove(aDelim);
+    Result:=true;
+  finally
+    if not Result then
+      Scanner.GotoBookmark;
+    lBuilder.free;
+  end;
+end;
+
+procedure TInlineTextProcessor.HandleEmphasis(aTerminator : TMarkDownDelimiter);
+
+const
+  NodeStyles : Array[Boolean] of TNodeStyle = (nsEmph,nsStrong);
+
+var
+  lCount,lClose,lOpen,lBottom,lCurrBottom,lRemoveLen,lDelete :  integer;
+  lStrong : boolean;
+  lBottoms : Array[Boolean] of Integer; // False: strong or more, True: emph
+  lOpens,lCloses : TMarkDownDelimiter;
+
+  function IsMatch(aDelim1,aDelim2 : TMarkDownDelimiter) : boolean;
+
+  begin
+    Result:=aDelim2.CanClose(aDelim1);
+    if Result and (aDelim1.OpensCloses or aDelim2.OpensCloses) then
+      Result:=(Length(aDelim1.Delimiter)+Length(aDelim2.Delimiter)) mod 3 <> 0;
+  end;
+
+  Function FindClosingDelimiter(aStart : integer) : Integer;
+  var
+    lIdx : Integer;
+  begin
+    lIdx:=aStart;
+    while (lIdx<lCount) and FStack[lIdx].isEmph and not FStack[lIdx].Closes do
+      inc(lIdx);
+    Result:=lIdx;
+  end;
+
+  Function FindOpeningDelimiter(aClose : integer) : Integer;
+  var
+    lIdx: integer;
+  begin
+    lIdx:=aClose-1;
+    while (lIdx>=lCurrBottom) and not isMatch(FStack[lIdx],FStack[aClose]) do
+      dec(lIdx);
+    Result:=lIdx;
+  end;
+
+begin
+  lBottom:=0;
+  if Assigned(aTerminator) then
+    lBottom:=FStack.IndexOf(aTerminator)+1;
+  lBottoms[False]:=lBottom;
+  lBottoms[True]:=lBottom;
+  lCount:=FStack.count;
+  lClose:=FindClosingDelimiter(lBottom);
+  while lClose<lCount do
+    begin
+    lCloses:=FStack[lClose];
+    // Determine bottom for this match
+    lCurrBottom:=Max(lBottoms[lCloses.delimiter='*'],lBottom);
+    lOpen:=FindOpeningDelimiter(lClose);
+    if (lOpen>=0) and lCloses.CanClose(FStack[lOpen]) then
+      begin
+      // we have a match
+      lOpens:=FStack[lOpen];
+      lStrong:=(Length(lCloses.Delimiter)>1) and (Length(lOpens.Delimiter)>1);
+      // apply the correct style
+      Nodes.ApplyStyleBetween(lOpens.Node,lCloses.node,NodeStyles[lStrong]);
+      // Remove the * or **
+      lRemoveLen:=1+Ord(lStrong);
+      lOpens.RemoveLast(lRemoveLen);
+      lCloses.RemoveLast(lRemoveLen);
+      // Remove delimiters in between opening and closing delimiter
+      lDelete:=lClose;
+      While lDelete>lOpen+1 do
+        begin
+        FStack.Delete(lDelete);
+        Dec(lDelete);
+        Dec(lClose);
+        end;
+      // if we have removed the last *, remove opening.
+      if lOpens.Delimiter='' then
+        begin
+        FStack.Delete(lOpen);
+        Dec(lClose);
+        end;
+      // if we have removed the last *, remove closing node.
+      // If the attached node is empty, remove that too.
+      if lCloses.Delimiter='' then
+        begin
+        if lCloses.node.isEmpty then
+          Nodes.Remove(lCloses.node);
+        FStack.Remove(lCloses);
+        end;
+      end
+    else
+      begin
+      lBottoms[FStack[lClose].delimiter='*']:=lOpen+1;
+      if FStack[lClose].Modes=dmOpenClose then
+        inc(lClose)
+      else
+        FStack.Delete(lClose);
+      end;
+    lCount:=FStack.Count;
+    lClose:=FindClosingDelimiter(lClose);
+    end;
+  for LOpen:=FStack.Count-1 downto lBottom do
+    FStack.Delete(lOpen);
+end;
+
+
+procedure TInlineTextProcessor.HandleCloseDelimiter();
+
+var
+  I : integer;
+  lDelim : TMarkDownDelimiter;
+
+begin
+  lDelim:=nil;
+  I:=FStack.Count-1;
+  while (I>=0) and (lDelim=Nil) do
+    begin
+    lDelim:=FStack[I];
+    if IndexText(lDelim.delimiter,['[','!['])=-1 then
+      lDelim:=nil;
+    Dec(I);
+    end;
+  if Not Assigned(lDelim) then
+    Nodes.addText(Scanner.location,Scanner.NextChar)
+  else if Not (lDelim.active and HandleInlineLink(lDelim)) then
+    begin
+    FStack.Remove(lDelim);
+    Nodes.addText(Scanner.location,Scanner.NextChar);
+    end;
+end;
+
+
+procedure TInlineTextProcessor.HandleTextCore;
+
+begin
+  // code blocks leave whitespace intact
+  if WhiteSpaceMode=wsLeave then
+    begin
+    Nodes.addText(Scanner.location,htmlEscape(Scanner.NextChar));
+    Exit;
+    end;
+  case Scanner.Peek of
+    '\' : HandleTextEscape();
+    '<' : HandleAutoLink();
+    '>','"' : Nodes.addText(Scanner.location,htmlEscape(Scanner.NextChar));
+    '&' : HandleEntity;
+    '`' : HandleBackTick;
+    '~' : HandleTilde;
+    '*' : HandleDelimiter(true);
+    '_' : if isWhitespaceChar(Scanner.PeekPrevious)
+             or MustEscape(Scanner.PeekPrevious)
+             or (Scanner.PeekPrevious=#0)
+             or isWhitespaceChar(Scanner.PeekEndRun)
+             or MustEscape(Scanner.PeekEndRun)
+             or (Scanner.PeekEndRun=#0) then
+            HandleDelimiter(true)
+          else
+            Nodes.addText(Scanner.Location,htmlEscape(Scanner.NextEquals()));
+    '[' : HandleDelimiter(false);
+    '!' : if Scanner.PeekNext='[' then
+            HandleDelimiter(false)
+          else
+            Nodes.addText(Scanner.location,Scanner.NextChar);
+    ']' : HandleCloseDelimiter();
+  else
+    if GFMExtensions then
+      HandleGFMExtensions
+    else
+      Nodes.addText(Scanner.location,Scanner.NextChar);
+  end;
+  //  DumpNodes;
+end;
+
+
+procedure TInlineTextProcessor.HandleGFMExtensions;
+
+var
+  lLen : integer;
+
+begin
+  if Scanner.has('www.') then
+    HandleGFMLinkURL(4,'http://')
+  else if Scanner.has('http://') then
+    HandleGFMLinkURL(7)
+  else if Scanner.has('https://') then
+    HandleGFMLinkURL(8)
+  else if Scanner.has('ftp://') then
+    HandleGFMLinkURL(6)
+  else if PeekEmailAddress(lLen) then
+    HandleGFMLinkEmail(lLen)
+  else
+    Nodes.addText(Scanner.location,Scanner.NextChar);
+end;
+
+
+function TInlineTextProcessor.PeekEmailAddress(out len : integer) : boolean;
+
+const
+  cEMailChars = ['a'..'z','A'..'Z','0'..'9','+','-','_','@','.'];
+
+var
+  lLen : integer;
+  cLast : Char;
+  S : String;
+
+begin
+  Result:=False;
+  S:=Scanner.PeekWhile(cEmailChars);
+  lLen:=Length(S);
+  cLast:=s[lLen];
+  // Strip off _ or -
+  if CharInSet(cLast,['_','-']) then
+    Exit;
+  if cLast='.' then
+    SetLength(S,lLen-1);
+  Result:=IsValidEmail(S);
+  if Result then
+    Len:=Length(s);
+end;
+
+
+function TInlineTextProcessor.process(aCloseLast: boolean): TMarkDownTextNodeList;
+
+var
+  c : Char;
+  lWhiteSpace : string;
+  lHadNonWhitespace : boolean;
+
+begin
+  Result:=Nil;
+  lWhiteSpace:='';
+  lHadNonWhitespace:=false;
+  while not Scanner.EOF do
+    begin
+    C:=Scanner.Peek;
+    if (WhiteSpaceMode<>wsLeave) and isWhitespace(C) and ((C<>#10) or (WhiteSpaceMode=wsStrip)) then
+      begin
+      C:=Scanner.NextChar;
+      if lHadNonWhitespace then
+        lWhitespace:=lWhitespace+c;
+      end
+    else
+      begin
+      if (WhiteSpaceMode<>wsLeave) then
+        begin
+        lHadNonWhitespace:=C<>#10;
+        if lHadNonWhitespace then
+          begin
+          if lWhiteSpace<>'' then
+            begin
+            if WhiteSpaceMode=wsStrip then
+              Nodes.addText(Scanner.location,' ')
+            else
+              Nodes.addText(Scanner.location,lWhitespace);
+            lWhiteSpace:='';
+            end;
+          end
+        else
+          begin
+          if lWhitespace.EndsWith('  ') then
+            Nodes.addText(Scanner.location,'<br />');
+          lWhitespace:='';
+          end;
+        end;
+      HandleTextCore();
+      end;
+    end;
+  HandleEmphasis(nil);
+  if aCloseLast and (Nodes.Count>0) then
+    Nodes.lastNode.Active:=False;
+end;
+
+
+procedure TInlineTextProcessor.HandleGFMLinkEmail(aLength : integer);
+
+var
+  lEmail : String;
+  lNode : TMarkDownTextNode;
+
+begin
+  lEmail:=Scanner.NextChars(aLength);
+  lNode:=Nodes.addTextNode(Scanner.location,nkUri,htmlEscape(lEmail));
+  lNode.attrs.Add('href','mailto:'+urlEscape(lEmail));
+end;
+
+procedure TInlineTextProcessor.HandleGFMLinkURL(aStartChars : integer; const aProtocol : String='');
+
+Const
+  URLChars = ['a'..'z','A'..'Z','0'..'9','_','-','.'];
+  URLDelims = [' ',#10,'<'];
+
+var
+  lRewind : boolean;
+  lRemove : Integer;
+  lLink,lUrl,lText : string;
+  lNode : TMarkDownTextNode;
+
+begin
+  Scanner.BookMark;
+  lRewind:=True;
+  try
+    lLink:=Scanner.NextChars(aStartChars);
+    while CharInSet(Scanner.Peek,UrlChars) do
+      lLink:=lLink+Scanner.NextChar;
+    if not lLink.Contains('.') then
+      Exit;
+    lRewind:=False;
+    while not (Scanner.EOF or CharInSet(Scanner.Peek,URLDelims)) do
+      lLink:=lLink+Scanner.NextChar;
+    case lLink[lLink.Length] of
+      '?','!','.',',',':','*','_','~':
+        lRemove:=1;
+      ')':
+        lRemove:=Ord(lLink.CountChar('(')<lLink.CountChar(')'));
+      ';':
+        lRemove:=CheckForTrailingEntity(lLink);
+    else
+      lRemove:=0;
+    end;
+    if lRemove=0 then
+      begin
+      lNode:=Nodes.addTextNode(Scanner.location,nkEmail,htmlEscape(lLink));
+      lNode.attrs.Add('href',aProtocol+urlEscape(lLink));
+      end
+    else
+      begin
+      lUrl:=Copy(lLink,1,Length(lLink)-lRemove);
+      lNode:=Nodes.addTextNode(Scanner.location,nkURI,aProtocol+htmlEscape(lUrl));
+      lNode.attrs.Add('href',aProtocol+urlEscape(lUrl));
+      lText:=Copy(lLink,Length(lLink)-lRemove,lRemove);
+      Nodes.addTextNode(Scanner.location,nkText,htmlEscape(lText));
+      end;
+  finally
+    if lRewind then
+      begin
+      Scanner.GotoBookmark;
+      Nodes.addText(Scanner.location,Scanner.NextChar);
+      end;
+  end;
+end;
+
+end.
+

+ 179 - 0
packages/fcl-md/src/markdown.line.pas

@@ -0,0 +1,179 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown single markdown line
+
+    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.Line;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses Markdown.Utils;
+
+type
+  { TMarkDownLine }
+
+  TMarkDownLine = class
+  private
+    FLine : AnsiString;
+    FLineNo : integer;
+    FCursor : integer;
+    FMark : integer;
+  public
+    constructor create(aLine : AnsiString; aLineNo : integer);
+    procedure Reset;
+    procedure Mark;
+    procedure Rewind;
+    procedure Advance(len : integer); // advance x number of spaces or characters
+    // Skip whitespace from the current cursor position. Returns first non-whitespace character
+    Function SkipWhiteSpace : Char;
+    // Is the cursor at end of line ?
+    function IsEmpty : boolean;
+    // Remainder of the line, starting at cursor
+    function Remainder : AnsiString;
+    // Returns count of whitespaces from cursor. Tab acts as 4 position tab.
+    function LeadingWhitespace : integer; inline;
+    // Returns count of whitespaces from cursor. Tab acts as 4 position tab.
+    // returns first non-whitespace character
+    function LeadingWhitespace(out aFirstNonWhitespaceChar : Char) : integer;
+    function isWhitespace : boolean; // if everything after cursor is whitespace
+    // Line is the text with initial tabs replaced by spaces.
+    property Line : AnsiString Read FLine;
+    property LineNo : integer Read FLineNo;
+
+  end;
+  TMarkDownLineList = class (specialize TGFPObjectList<TMarkDownLine>);
+
+implementation
+
+uses
+{$IFDEF FPC_DOTTEDUNITS}
+  System.Classes, System.SysUtils;
+{$ELSE}
+  Classes, SysUtils;
+{$ENDIF}  
+
+{ TMarkDownLine }
+
+constructor TMarkDownLine.create(aLine: AnsiString; aLineNo: integer);
+
+begin
+  inherited create;
+  FLine:=TransformTabs(aline);
+  FLineNo:=aLineNo;
+  Reset;
+end;
+
+
+procedure TMarkDownLine.Reset;
+
+begin
+  FCursor:=1;
+end;
+
+
+procedure TMarkDownLine.Rewind;
+begin
+  FCursor:=FMark;
+end;
+
+
+procedure TMarkDownLine.Advance(len: integer);
+begin
+  inc(FCursor,len);
+end;
+
+
+function TMarkDownLine.isEmpty: boolean;
+
+begin
+  Result:=FCursor>Length(FLine);
+end;
+
+
+function TMarkDownLine.isWhitespace: boolean;
+
+var
+  i,lLen: integer;
+
+begin
+  Result:=true;
+  lLen:=Length(FLine);
+  i:=FCursor;
+  While Result and (i<=lLen) do
+    begin
+    Result:=isWhitespaceChar(Fline[i]);
+    inc(I);
+    end;
+end;
+
+procedure TMarkDownLine.Mark;
+begin
+  FMark:=FCursor;
+end;
+
+
+function TMarkDownLine.LeadingWhitespace : integer;
+var
+  lDummy : char;
+begin
+  Result:=LeadingWhitespace(lDummy);
+end;
+
+function TMarkDownLine.LeadingWhitespace(out aFirstNonWhitespaceChar: Char) : integer;
+
+var
+  lIndex,lLen : integer;
+
+begin
+  Result:=0;
+  lIndex:=FCursor;
+  lLen:=Length(FLine);
+  while (lIndex<=lLen) and isWhitespaceChar(FLine[lindex]) do
+    begin
+    inc(Result,1);
+    inc(lIndex);
+    end;
+  if (lIndex<=lLen) then
+    aFirstNonWhitespaceChar:=Fline[lIndex]
+  else
+    aFirstNonWhitespaceChar:=#0;
+end;
+
+
+function TMarkDownLine.Remainder: AnsiString;
+
+begin
+  Result:=Copy(FLine,FCursor,Length(FLine)-FCursor+1);
+end;
+
+
+function TMarkDownLine.SkipWhiteSpace: Char;
+
+var
+  lLen : Integer;
+
+begin
+  lLen:=Length(FLine);
+  while (FCursor<=lLen) and isWhitespaceChar(FLine[FCursor]) do
+    inc(FCursor);
+  if FCursor<lLen then
+    Result:=FLine[FCursor+1]
+  else
+    Result:=#0;
+end;
+
+
+end.
+

+ 816 - 0
packages/fcl-md/src/markdown.parser.pas

@@ -0,0 +1,816 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown block structure parser.
+
+    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.Parser;
+
+{$mode objfpc}
+{$h+}
+interface
+
+uses
+{$IFDEF FPC_DOTTEDUNITS}
+  System.CodePages.UnicodeData, System.SysUtils, System.Classes, System.Contnrs,
+{$ELSE}
+  UnicodeData, SysUtils, Classes, Contnrs,
+{$ENDIF}  
+  MarkDown.Elements,
+  MarkDown.Utils,
+  MarkDown.Scanner,
+  MarkDown.Line,
+  MarkDown.InlineText,
+  MarkDown.HtmlEntities;
+
+type
+  EMarkDown = class(Exception);
+  // Forward definition
+  TMarkDownParser = class;
+
+  // Options
+  TMarkDownOption = (mdoGithubFlavoured);
+  TMarkDownOptions = set of TMarkDownOption;
+
+  // Parent block context
+  TMarkDownBlockProcessingContext = (bpGeneral, bpCodeBlock, bpFencedCodeBlock);
+
+  { TMarkDownBlockProcessor }
+  TMarkDownBlockProcessor = class abstract (TObject)
+  private
+    FParser : TMarkdownParser;
+    function GetParentProcessor: TMarkDownBlockProcessor;
+  protected
+    function inListOrQuote : boolean; virtual;
+    // Access to parser methods
+    function isList(ordered : boolean; const marker : String; indent : integer) : boolean; virtual;
+    function PeekLine : TMarkDownLine;
+    function NextLine : TMarkDownLine;
+    function Done : Boolean;
+    procedure RedoLine(aResetLine: Boolean);
+    function InList(blocks : TMarkDownBlockList; ordered : boolean; marker : String; indent : integer; grace : integer; out list : TMarkDownListBlock) : boolean;
+    function IsBlock(aBlock : TMarkDownBlock; blocks : TMarkDownBlockList; const aLine : String; wsLen : integer = 3) : boolean;
+    function CurrentLine : TMarkDownLine;
+    procedure Parse(aParent: TMarkDownContainerBlock; aPArentProcessor: TMarkDownBlockProcessor); overload;
+    // Our parser
+    Property parser : TMarkDownParser read FParser;
+    // Parent processor
+    property ParentProcessor : TMarkDownBlockProcessor Read GetParentProcessor;
+
+  public
+    // One instance is created for each block type.
+    constructor Create(aParser : TMarkdownParser); virtual;
+    // Register for given block type
+    class procedure register(const aBlockType : String);
+    // This is called to see whether aLine closes the current block.
+    function LineEndsBlock(aBlock : TMarkDownContainerBlock; aLine: TMarkDownLine): Boolean; virtual;
+    // Return true if this processor handles the current line. Needs to handle state.
+    function HandlesLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine): boolean; virtual; abstract;
+    // When HandlesLine returned true, ProcessLine is called.
+    // If ProcessLine returned true, the next line is started.
+    // If it is false, the line is given to another processor.
+    // processline is where a new block is created and attached to the parent block.
+    function processLine(aParent : TMarkDownContainerBlock; aLine : TMarkDownLine; context : TMarkDownBlockProcessingContext) : boolean; virtual; abstract;
+  end;
+  TMarkDownBlockProcessorArray = array of TMarkDownBlockProcessor;
+  TMarkDownBlockProcessorClass = class of TMarkDownBlockProcessor;
+  TMarkDownBlockProcessorClassArray = array of TMarkDownBlockProcessorClass;
+
+  { TMarkDownDocumentProcessor }
+
+  // We always need this one. It is not registered, but created hardcoded
+  TMarkDownDocumentProcessor = class (TMarkDownBlockProcessor)
+  public
+    function processLine(aParent : TMarkDownContainerBlock; aLine : TMarkDownLine; aContext : TMarkDownBlockProcessingContext) : Boolean; override;
+    function HandlesLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine): boolean; override;
+  end;
+
+
+  { TMarkDownParser }
+
+  TMarkDownParser = class(TComponent)
+  private
+    FLazy: Boolean;
+    FLines : TMarkDownLineList;
+    FCurrentLine : integer;
+    FBuilder : TStringBuilder;
+    FEntities : TFPStringHashTable;
+    FOptions : TMarkDownOptions;
+    FProcessors : TMarkDownBlockProcessorArray;
+    FProcessorStack : TStack;
+    function CreateScanner(const aText: String; aStartLine: Integer): TMarkDownTextScanner;
+  Protected
+    // Collect all entitues
+    procedure CollectEntities(aList: TFPStringHashTable);
+    // aLine operations
+    // Convert strings in lines to TMarkDownLine instances
+    procedure ConvertLines(aLines : TStrings);
+    // Get the current line (can be nil);
+    function CurrentLine : TMarkDownLine;
+    // Get the next line (can be nil), but do not move the current line pointer
+    function PeekLine : TMarkDownLine;
+    // Get the next line (can be nil) and move the current line pointer
+    function NextLine : TMarkDownLine;
+    // Should we re-process the current line ? Moves line pointer one back, optionally resets the line.
+    procedure RedoLine(aResetLine : Boolean = True);
+    // is there a line available for redo ?
+    function CanRedo : boolean;
+    // Have we reached the last line ?
+    function Done : boolean;
+    // status
+    // Is the last block a list with the given properties ?
+    // if yes, return the list
+    function InList(aBlocks: TMarkDownBlockList; aOrdered: boolean; const aMarker: String; aIndent: integer; aGrace: integer; out
+      aList: TMarkDownListBlock): boolean;
+    // Does aLine start a new block (true) or can it be a continuation (false) ?
+    function IsBlock(aParent: TMarkDownBlock; aBlocks: TMarkDownBlockList; const aLine: String; aWhiteSpaceLen: integer): boolean;
+    // block parsing loop
+    procedure Parse(aParent: TMarkDownContainerBlock; aPArentProcessor: TMarkDownBlockProcessor); overload;
+    // Parent processor of current processor
+    function ParentProcessor : TMarkDownBlockProcessor;
+    // process a text line
+    function ProcessText(const aText: String; wsMode: TWhitespaceMode; aStartLine: integer): TMarkDownTextNodeList;
+    // Recursively process inline text
+    procedure ProcessInlines(aBlock : TMarkDownBlock; wsMode : TWhitespaceMode);
+    // Initialize processors
+    Procedure InitProcessors;
+    // Done with processors
+    Procedure DoneProcessors;
+    // To customize the Inline Text processor class, override this.
+    function GetInlineTextProcessorClass : TInlineTextProcessorClass; virtual;
+    // To customize top-level document class, override this.
+    function CreateDocument(aLine: integer): TMarkDownDocument; virtual;
+    // To customize top-level document bloc k parser, override this.
+    function CreateDocumentProcessor: TMarkDownDocumentProcessor; virtual;
+  public
+    Constructor Create(aOwner : TComponent); override;
+    Destructor Destroy; override;
+    // Utility function to handle parsing of inline text.
+    procedure ParseInline(aParent : TMarkDownContainerBlock; const aLine : String);
+    // Parse the markDown in strings
+    function Parse(aSource: TStrings): TMarkDownDocument; overload;
+    // Helper : is the last block a plain paragraph ?
+    class function InPara(blocks : TMarkDownBlockList; canBeQuote : boolean) : boolean;
+    // Helper to quickly parse a stringlist into a markdown document
+    class function FastParse(aSource: TStrings; aOptions: TMarkDownOptions): TMarkDownDocument;
+    // State control in lazy continuation .
+    property Lazy : Boolean Read FLazy Write FLazy;
+    // HTML entities to convert
+    Property Entities : TFPStringHashTable read FEntities;
+  published
+    // Options
+    Property Options : TMarkDownOptions Read FOptions Write FOptions;
+  end;
+
+  { TMarkDownProcessorFactory }
+
+  TMarkDownProcessorFactory = class(TObject)
+  Private
+    class var _instance : TMarkDownProcessorFactory;
+  private
+    Type
+      TRegisteredProcessor = class(TObject)
+        Name : string;
+        Processor : TMarkDownBlockProcessorClass;
+        constructor create(const aName : String; aProcessor : TMarkDownBlockProcessorClass);
+      end;
+      TRegisteredProcessorList = class(Specialize TGFPObjectList<TRegisteredProcessor>);
+  Private
+    FList : TRegisteredProcessorList;
+    function Find(const aBlockType : String; aAllowCreate : Boolean) : TRegisteredProcessor;
+  public
+    constructor Create;
+    destructor Destroy; override;
+    class constructor init;
+    class destructor done;
+    // Return an array with all known processors.
+    function All : TMarkDownBlockProcessorClassArray;
+    // Find a processor for a block of type aBlockType
+    function FindProcessor(const aBlockType : string) : TMarkDownBlockProcessorClass;
+    // Register a processor for block type aBlockType. Existing processor will be overwritten
+    Procedure RegisterProcessor(const aBlockType : String; aProcessor : TMarkDownBlockProcessorClass);
+    // Singleton instance
+    class property Instance : TMarkDownProcessorFactory Read _instance;
+  end;
+
+
+implementation
+
+{ TMarkDownBlockProcessor }
+
+constructor TMarkDownBlockProcessor.Create(aParser: TMarkdownParser);
+
+begin
+  inherited Create;
+  FParser:=aParser;
+end;
+
+
+class procedure TMarkDownBlockProcessor.register(const aBlockType: String);
+
+begin
+  TMarkDownProcessorFactory.Instance.RegisterProcessor(aBlockType,Self);
+end;
+
+
+function TMarkDownBlockProcessor.LineEndsBlock(aBlock: TMarkDownContainerBlock; aLine: TMarkDownLine): Boolean;
+
+begin
+  Result:=(aLine=Nil) or (aBlock=Nil);
+end;
+
+
+function TMarkDownBlockProcessor.inListOrQuote: boolean;
+
+begin
+  // Todo
+  Result:=false;
+end;
+
+
+function TMarkDownBlockProcessor.PeekLine: TMarkDownLine;
+
+begin
+  Result:=FParser.PeekLine;
+end;
+
+
+function TMarkDownBlockProcessor.NextLine: TMarkDownLine;
+
+begin
+  Result:=FParser.NextLine;
+end;
+
+
+function TMarkDownBlockProcessor.Done: Boolean;
+
+begin
+  Result:=FParser.Done;
+end;
+
+
+procedure TMarkDownBlockProcessor.RedoLine(aResetLine: Boolean);
+
+begin
+  FParser.RedoLine(aResetLine);
+end;
+
+
+function TMarkDownBlockProcessor.InList(blocks: TMarkDownBlockList; ordered: boolean; marker: String; indent: integer;
+  grace: integer; out list: TMarkDownListBlock): boolean;
+
+begin
+  Result:=FParser.InList(blocks,ordered,marker,indent,grace,list);
+end;
+
+
+function TMarkDownBlockProcessor.IsBlock(aBlock: TMarkDownBlock; blocks: TMarkDownBlockList; const aLine: String; wsLen: integer
+  ): boolean;
+
+begin
+  Result:=FParser.IsBlock(aBlock,Blocks,aLine,wsLen);
+end;
+
+
+function TMarkDownBlockProcessor.CurrentLine: TMarkDownLine;
+
+begin
+  Result:=FParser.CurrentLine;
+end;
+
+
+procedure TMarkDownBlockProcessor.Parse(aParent: TMarkDownContainerBlock; aPArentProcessor: TMarkDownBlockProcessor);
+
+begin
+  FParser.Parse(aParent,aParentProcessor);
+end;
+
+
+function TMarkDownBlockProcessor.GetParentProcessor: TMarkDownBlockProcessor;
+
+begin
+  Result:=FParser.ParentProcessor;
+end;
+
+function TMarkDownBlockProcessor.isList(ordered: boolean; const marker: String; indent: integer): boolean;
+
+begin
+  Result:=false;
+  if ordered and (marker<>'') and (indent>0) then ; // keep compiler happy
+end;
+
+
+{ TMarkDownDocumentProcessor }
+
+function TMarkDownDocumentProcessor.processLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine; aContext : TMarkDownBlockProcessingContext): Boolean;
+
+begin
+  Result:=False;
+end;
+
+
+function TMarkDownDocumentProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
+
+begin
+  Result:=False;
+end;
+
+
+{ TMarkDownParser }
+
+class function TMarkDownParser.FastParse(aSource: TStrings; aOptions: TMarkDownOptions): TMarkDownDocument;
+
+var
+  lParser : TMarkDownParser;
+  lDone : Boolean;
+
+begin
+  Result:=Nil;
+  lDone:=false;
+  lParser:=TMarkDownParser.Create(Nil);
+  try
+    lParser.Options:=aOptions;
+    Result:=LParser.Parse(aSource);
+    lDone:=true;
+  finally
+    if not lDone then
+      Result.Free;
+    lParser.free;
+  end;
+end;
+
+
+procedure TMarkDownParser.CollectEntities(aList :TFPStringHashTable);
+
+var
+  Ent : THTMLEntityDef;
+
+begin
+  for ent in EntityDefList do
+    aList.Add(ent.e,Utf8Encode(ent.u));
+end;
+
+
+constructor TMarkDownParser.Create(aOwner : TComponent);
+
+begin
+  inherited ;
+  FOptions:=[];
+  FBuilder:=TStringBuilder.Create;
+  FProcessorStack:=TStack.Create;
+  FLines:=TMarkDownLineList.create(true);
+  FEntities:=TFPStringHashTable.create;
+  CollectEntities(FEntities);
+end;
+
+
+destructor TMarkDownParser.Destroy;
+
+begin
+  FreeAndNil(FEntities);
+  FreeAndNil(FProcessorStack);
+  FreeAndNil(FBuilder);
+  FreeAndNil(FLines);
+  inherited;
+end;
+
+
+function TMarkDownParser.CreateDocument(aLine : integer) : TMarkDownDocument;
+
+begin
+  Result:=TMarkDownDocument.Create(Nil,aLine);
+end;
+
+
+function TMarkDownParser.CreateDocumentProcessor : TMarkDownDocumentProcessor;
+
+begin
+  Result:=TMarkDownDocumentProcessor.Create(Self);
+end;
+
+function TMarkDownParser.Parse(aSource: TStrings): TMarkDownDocument;
+
+var
+  lProc : TMarkDownDocumentProcessor;
+  lDone : Boolean;
+
+begin
+  Result:=Nil;
+  lProc:=Nil;
+  lDone:=false;
+  try
+    InitProcessors;
+    ConvertLines(aSource);
+    lProc:=CreateDocumentProcessor;
+    Result:=CreateDocument(1);
+    parse(Result,lProc);
+    processInlines(Result, wsTrim);
+    lDone:=True;
+    Result.closed:=True;
+  finally
+    DoneProcessors;
+    if not lDone then
+      Result.Free;
+    lProc.Free;
+  end;
+end;
+
+
+function TMarkDownParser.NextLine: TMarkDownLine;
+
+begin
+  Result:=Nil;
+  inc(FCurrentLine);
+  if FCurrentLine<FLines.Count then
+    Result:=FLines[FCurrentLine];
+end;
+
+
+function TMarkDownParser.PeekLine: TMarkDownLine;
+
+begin
+  Result:=nil;
+  if (FCurrentLine<FLines.Count-1) then
+    Result:=FLines[FCurrentLine+1];
+end;
+
+
+procedure TMarkDownParser.RedoLine(aResetLine: Boolean);
+
+begin
+  if aResetLine then
+    FLines[FCurrentLine].reset;
+  dec(FCurrentLine);
+end;
+
+
+function TMarkDownParser.CanRedo: boolean;
+
+begin
+  Result:=(FCurrentLine<FLines.Count);
+end;
+
+
+function TMarkDownParser.Done: boolean;
+begin
+  Result:=(FCurrentLine>=FLines.Count-1)
+end;
+
+
+function TMarkDownParser.InList(aBlocks: TMarkDownBlockList; aOrdered: boolean; const aMarker: String; aIndent: integer; aGrace: integer; out aList: TMarkDownListBlock): boolean;
+
+begin
+  Result:=(aBlocks.Count > 0) and (aBlocks.Last is TMarkDownListBlock);
+  if Not Result then
+    exit;
+  aList:=aBlocks.Last as TMarkDownListBlock;
+  Result:=(aList.ordered=aOrdered)
+          and (aList.Marker=aMarker)
+          and (aIndent-aGrace<=aList.LastIndent)
+          and not aList.closed
+end;
+
+
+class function TMarkDownParser.InPara(blocks: TMarkDownBlockList; canBeQuote: boolean): boolean;
+
+begin
+  Result:=(blocks.Count > 0)
+            and (blocks.Last is TMarkDownParagraphBlock)
+            and not (blocks.Last as TMarkDownParagraphBlock).closed
+            and ((blocks.Last as TMarkDownParagraphBlock).header = 0);
+  if Result and not canBeQuote and not (blocks.Last as TMarkDownParagraphBlock).isPlainPara then
+    Result:=false;
+end;
+
+
+function TMarkDownParser.IsBlock(aParent: TMarkDownBlock; aBlocks: TMarkDownBlockList; const aLine: String; aWhiteSpaceLen: integer): boolean;
+
+  function inOrderedList : boolean;
+  begin
+    Result:=(aParent is TMarkDownListBlock) and (aParent as TMarkDownListBlock).Ordered;
+  end;
+
+  function IsOpenPara : boolean;
+  begin
+    Result:=(aBlocks.count > 0) and (aBlocks.lastblock is TMarkDownParagraphBlock) and not (aBlocks.LastBlock.Closed)
+  end;
+
+var
+  lSkip : integer;
+  lLine,lMarker : String;
+
+begin
+  // Known blocks
+  Result:=True;
+  if StartsWithWhitespace(aLine, ['*','-','+','#','`','~','>'],lSkip,aWhiteSpaceLen) then
+    Exit;
+  // Thematic break
+  if StartsWithWhitespace(aLine, '___',lSkip) then
+    Exit;
+  // Code block
+  if (LeadingWhitespace(aLine) >= 4) and not InPara(aBlocks,False) then
+    Exit;
+  // open para ?
+  Result:=Not IsOpenPara;
+  if Result then
+    exit;
+  // Remove whitespace
+  lSkip:=LeadingWhitespace(aLine);
+  lLine:=RemoveLeadingWhiteSpace(aLine,lSkip);
+  // Check ordered List item.
+  lMarker:=CopyMatching(lLine, ['0'..'9']);
+  if (lMarker='') then
+    Exit;
+  // 1. is always a new block. In an ordered list we must check.
+  if (lMarker='1') or inOrderedList then
+    begin
+    Delete(lLine,1,Length(lMarker));
+    if (lLine<>'') then
+      begin
+      Result:=(lLine[1] in [')','.']);
+      if Result then
+        Result:=(length(lLine)>1) and (lLine[2]=' ');
+      end;
+    end;
+end;
+
+
+procedure TMarkDownParser.ParseInline(aParent: TMarkDownContainerBlock; const aLine: String);
+
+var
+  lBlock : TMarkDownTextBlock;
+
+begin
+  if (aParent.blocks.Count > 0) and (aParent.blocks.Last is TMarkDownTextBlock) then
+    lBlock:=aParent.blocks.Last as TMarkDownTextBlock
+  else
+    lBlock:=TMarkDownTextBlock.create(aParent,FCurrentLine,'');
+  if lBlock.Text<>'' then
+    lBlock.Text:=lBlock.Text+#10;
+  lBlock.Text:=lBlock.Text+aLine;
+end;
+
+
+procedure TMarkDownParser.Parse(aParent: TMarkDownContainerBlock; aPArentProcessor: TMarkDownBlockProcessor);
+
+var
+  lLine : TMarkDownLine;
+  lprocessor : TMarkDownBlockProcessor;
+  i,lProcCount : Integer;
+  lProcessed : boolean;
+
+begin
+  FProcessorStack.Push(aParentProcessor);
+  try
+    Lazy:=False;
+    while not done do
+      begin
+      if aParentProcessor.LineEndsBlock(aParent,PeekLine) then
+        exit;
+      lLine:=NextLine;
+      lProcessed:=False;
+      I:=0;
+      lProcCount:=Length(FProcessors);
+      While (not lProcessed) and (I<lProcCount) do
+        begin
+        lprocessor:=FProcessors[i];
+        lProcessed:=lprocessor.HandlesLine(aParent,lLine);
+        if lProcessed then
+           lProcessed:=lprocessor.processLine(aParent,lLine,bpGeneral);
+        // The last processor is normally the paragraph block...
+        inc(I);
+        end;
+      if not lProcessed then
+        Raise EMarkDown.CreateFmt('Line %s not processed',[lLine.LineNo]);
+      end;
+    if aParent.ChildCount>0 then
+      aParent[aParent.ChildCount-1].closed:=True;
+  finally
+    FProcessorStack.Pop;
+  end;
+end;
+
+
+procedure TMarkDownParser.ConvertLines(aLines: TStrings);
+
+var
+  i : integer;
+
+begin
+  FLines.Clear;
+  For I:=0 to aLines.Count-1 do
+    FLines.Add(TMarkDownLine.Create(aLines[i],I+1));
+  FCurrentLine:=-1;
+end;
+
+function TMarkDownParser.CurrentLine: TMarkDownLine;
+
+begin
+  Result:=Nil;
+  if (FCurrentLine>=0) and (FCurrentLine<FLines.Count) then
+    Result:=FLines[FCurrentLine];
+end;
+
+
+function TMarkDownParser.ParentProcessor: TMarkDownBlockProcessor;
+
+begin
+  Result:=TMarkDownBlockProcessor(FProcessorStack.Peek);
+end;
+
+function TMarkDownParser.CreateScanner(const aText : String; aStartLine : Integer) : TMarkDownTextScanner;
+begin
+  Result:=TMarkDownTextScanner.Create(aText,aStartLine);
+end;
+
+function TMarkDownParser.ProcessText(const aText: String; wsMode: TWhitespaceMode; aStartLine: integer): TMarkDownTextNodeList;
+
+var
+  Scanner : TMarkDownTextScanner;
+  Processor : TInlineTextProcessor;
+  lClass : TInlineTextProcessorClass;
+begin
+  Result:=TMarkDownTextNodeList.Create;
+  Scanner:=CreateScanner(aText,aStartLine);
+  try
+    lClass:=GetInlineTextProcessorClass;
+    Processor:= LClass.Create(Scanner,Result,FEntities,wsMode);
+    Processor.GFMExtensions:=mdoGithubFlavoured in Options;
+    Processor.process(true);
+  finally
+    Scanner.Free;
+    Processor.Free;
+  end;
+end;
+
+
+procedure TMarkDownParser.ProcessInlines(aBlock: TMarkDownBlock; wsMode: TWhitespaceMode);
+
+var
+  I : Integer;
+  lTextBlock : TMarkDownTextBlock absolute aBlock;
+
+begin
+  if aBlock is TMarkDownTextBlock then
+    lTextBlock.Nodes:=processText(lTextBlock.Text,wsMode,aBlock.Line);
+  for I:=0 to aBlock.ChildCount-1 do
+    processInlines(aBlock.Children[i],aBlock.WhiteSpaceMode);
+end;
+
+
+procedure TMarkDownParser.InitProcessors;
+
+var
+  lClasses : TMarkDownBlockProcessorClassArray;
+  lPar : TMarkDownBlockProcessorClass;
+  lCount,I : Integer;
+
+begin
+  DoneProcessors;
+  lClasses:=TMarkDownProcessorFactory.Instance.All;
+  if Length(lClasses)=0 then
+    Raise EMarkDown.Create('No markdown processors registered');
+  lPar:=TMarkDownProcessorFactory.Instance.findprocessor('paragraph');
+  SetLength(FProcessors,Length(lClasses));
+  lCount:=0;
+  For I:=0 to Length(lClasses)-1 do
+    begin
+    if lClasses[i]<>lPar then
+      begin
+      FProcessors[lCount]:=lClasses[i].Create(Self);
+      inc(lCount);
+      end;
+    end;
+  FProcessors[lCount]:=LPar.Create(Self);
+end;
+
+
+procedure TMarkDownParser.DoneProcessors;
+
+var
+  I : Integer;
+
+begin
+  For I:=0 to Length(FProcessors)-1 do
+    FreeAndNil(FProcessors[i]);
+end;
+
+
+function TMarkDownParser.GetInlineTextProcessorClass: TInlineTextProcessorClass;
+
+begin
+  Result:=TInlineTextProcessor;
+end;
+
+
+{ TMarkDownProcessorFactory }
+
+function TMarkDownProcessorFactory.Find(const aBlockType: String; aAllowCreate: Boolean): TRegisteredProcessor;
+
+var
+  Idx : integer;
+
+begin
+  Result:=Nil;
+  Idx:=FList.Count-1;
+  While (Result=Nil) and (Idx>=0) do
+    begin
+    Result:=FList[Idx];
+    if not SameText(Result.Name,aBlockType) then
+      Result:=Nil;
+    Dec(Idx);
+    end;
+  if (Result=Nil) and aAllowCreate then
+    begin
+    Result:=TRegisteredProcessor.Create(aBlockType,Nil);
+    FList.Add(Result);
+    end;
+end;
+
+
+constructor TMarkDownProcessorFactory.Create;
+
+begin
+  FList:=TRegisteredProcessorList.Create(True);
+end;
+
+
+destructor TMarkDownProcessorFactory.Destroy;
+
+begin
+  FreeAndNil(FList);
+  inherited destroy;
+end;
+
+
+class constructor TMarkDownProcessorFactory.init;
+
+begin
+  _instance:=TMarkDownProcessorFactory.Create;
+end;
+
+
+class destructor TMarkDownProcessorFactory.done;
+
+begin
+  FreeAndNil(_instance);
+end;
+
+
+function TMarkDownProcessorFactory.All: TMarkDownBlockProcessorClassArray;
+
+var
+  i : integer;
+
+begin
+  Result:=[];
+  SetLength(Result,FList.Count);
+  For I:=0 to FList.Count-1 do
+    Result[I]:=FList[i].Processor;
+end;
+
+
+function TMarkDownProcessorFactory.FindProcessor(const aBlockType: string): TMarkDownBlockProcessorClass;
+
+var
+  lReg : TRegisteredProcessor;
+
+begin
+  Result:=Nil;
+  LReg:=Find(aBlockType,False);
+  if Assigned(lReg) then
+    Result:=lReg.Processor;
+end;
+
+
+procedure TMarkDownProcessorFactory.RegisterProcessor(const aBlockType: String; aProcessor: TMarkDownBlockProcessorClass);
+
+var
+  lReg : TRegisteredProcessor;
+
+begin
+  LReg:=Find(LowerCase(aBlockType),True);
+  lReg.Processor:=aProcessor;
+end;
+
+
+{ TMarkDownProcessorFactory.TRegisteredProcessor }
+
+constructor TMarkDownProcessorFactory.TRegisteredProcessor.create(const aName: String; aProcessor: TMarkDownBlockProcessorClass);
+
+begin
+  Name:=aName;
+  Processor:=aProcessor;
+end;
+
+
+end.

+ 1152 - 0
packages/fcl-md/src/markdown.processors.pas

@@ -0,0 +1,1152 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Basic Markdown block processors
+
+    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.Processors;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+{$IFDEF FPC_DOTTEDUNITS}
+  System.CodePages.unicodedata, System.Types,  System.SysUtils, 
+  System.Classes, System.StrUtils, System.Contnrs,
+{$ELSE}
+  UnicodeData, Types,  SysUtils, Classes, StrUtils, Contnrs,
+{$ENDIF}  
+  MarkDown.Elements,
+  MarkDown.Utils,
+  MarkDown.Line,
+  MarkDown.HtmlEntities,
+  MarkDown.Parser;
+
+type
+  { TMarkDownQuoteProcessor }
+
+  TMarkDownQuoteProcessor = class (TMarkDownBlockProcessor)
+  private
+    FLevel : integer;
+    function SkipQuotes(aLine: TMarkDownLine; aLevel: Integer): Integer;
+  protected
+    function IsQuotedLine(aLine: TMarkDownLine; SkipLevels: Boolean): boolean;
+    function inListOrQuote : boolean; override;
+    function IsRoot(aParent : TMarkDownContainerBlock) : Boolean;
+  public
+    function LineEndsBlock(aBlock: TMarkDownContainerBlock; aLine: TMarkDownLine): Boolean; override;
+    function HandlesLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine): boolean; override;
+    function processLine(aParent : TMarkDownContainerBlock; aLine : TMarkDownLine; aContext : TMarkDownBlockProcessingContext) : Boolean; override;
+  end;
+
+  { TMarkDownListProcessor }
+
+  TMarkDownListProcessor = class (TMarkDownBlockProcessor)
+  private
+    FHasContent : boolean;
+    FEmptyLine : integer;
+    FLastList : TMarkDownListBlock;
+    FLastItem : TMarkDownListItemBlock;
+    FIndent : Integer;
+    FMarker : String;
+  protected
+    function isList(aOrdered : boolean; const aMarker : String; aIndent : integer) : boolean; override;
+    function inListOrQuote : boolean; override;
+    function Root : Boolean;
+    function LastList : TMarkDownListBlock;
+  public
+    function prepareline(aLine : TMarkDownLine; aContext : TMarkDownBlockProcessingContext) : boolean;
+  end;
+
+  { TUListProcessor }
+
+  TUListProcessor = class (TMarkDownListProcessor)
+  private
+    function IsItemInList(aList: TMarkDownListBlock; aLine: TMarkDownLine): boolean;
+  public
+    function LineEndsBlock(aBlock: TMarkDownContainerBlock; aLine: TMarkDownLine): Boolean; override;
+    function HandlesLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine): boolean; override;
+    function processLine(aParent : TMarkDownContainerBlock; aLine : TMarkDownLine; aContext : TMarkDownBlockProcessingContext) : boolean; override;
+  end;
+
+  { TOListProcessor }
+
+  TOListProcessor = class (TMarkDownListProcessor)
+  private
+    FStart : Integer;
+    function IsItemInList(aList: TMarkDownListBlock; aLine: TMarkDownLine): boolean;
+  Public
+    function LineEndsBlock(aBlock: TMarkDownContainerBlock; aLine: TMarkDownLine): Boolean; override;
+    function HandlesLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine): boolean; override;
+    function processLine(aParent : TMarkDownContainerBlock; aLine : TMarkDownLine; aContext : TMarkDownBlockProcessingContext) : boolean; override;
+  end;
+
+  { TSeTextHeaderProcessor }
+
+  TSeTextHeaderProcessor = class(TMarkDownBlockProcessor)
+    function HandlesLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine): boolean; override;
+    function processLine(aParent : TMarkDownContainerBlock; aLine : TMarkDownLine; aContext : TMarkDownBlockProcessingContext) : boolean; override;
+  end;
+
+  { TThematicBreakProcessor }
+
+  TThematicBreakProcessor = class(TMarkDownBlockProcessor)
+    function HandlesLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine): boolean; override;
+    function processLine(aParent : TMarkDownContainerBlock; aLine : TMarkDownLine; aContext : TMarkDownBlockProcessingContext) : Boolean; override;
+  end;
+
+
+  { TCodeBlockProcessor }
+
+  TCodeBlockProcessor = class (TMarkDownBlockProcessor)
+  private
+    procedure ProcessCodeBlock(C: TMarkDownCodeBlock; aLine: TMarkDownLine);
+  public
+    function HandlesLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine): boolean; override;
+    function processLine(aParent : TMarkDownContainerBlock; aLine : TMarkDownLine; aContext : TMarkDownBlockProcessingContext) : Boolean; override;
+  end;
+
+  { TFencedCodeBlockProcessor }
+
+  TFencedCodeBlockProcessor = class (TMarkDownBlockProcessor)
+  Private
+    FIndent : Integer;
+    FLang : String;
+    FTerminal: string;
+  Public
+    function LineEndsBlock(aBlock: TMarkDownContainerBlock; aLine: TMarkDownLine): Boolean; override;
+    function HandlesLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine): boolean; override;
+    function processLine(aParent : TMarkDownContainerBlock; aLine : TMarkDownLine; aContext : TMarkDownBlockProcessingContext) : Boolean; override;
+  end;
+
+  { TMarkDownHeadingProcessor }
+
+  TMarkDownHeadingProcessor = class(TMarkDownBlockProcessor)
+    FLen : Integer;
+    function HandlesLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine): boolean; override;
+    function processLine(aParent : TMarkDownContainerBlock; aLine : TMarkDownLine; aContext : TMarkDownBlockProcessingContext) : Boolean; override;
+  end;
+
+  { TTableProcessor }
+
+  TTableProcessor = class(TMarkDownBlockProcessor)
+  private
+    function CountCells(aLine: TMarkDownLine): Integer;
+  protected
+    procedure ParseTableLine(aTable: TMarkDownTableBlock; aLine: TMarkDownLine);
+    function IsEndOfTable(aLine : TMarkDownLine) : boolean;
+    // Check that number of cells in second line of table is the same as in first line
+    function Strict : boolean; virtual;
+  public
+    class var
+      DefaultStrict : Boolean;
+    function HandlesLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine): boolean; override;
+    function processLine(aParent : TMarkDownContainerBlock; aLine : TMarkDownLine; aContext : TMarkDownBlockProcessingContext) : Boolean; override;
+  end;
+
+  { TParagraphProcessor }
+
+  TParagraphProcessor = class(TMarkDownBlockProcessor)
+  public
+    function processLine(aParent : TMarkDownContainerBlock; aLine : TMarkDownLine; aContext : TMarkDownBlockProcessingContext) : Boolean; override;
+    function HandlesLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine): boolean; override;
+  end;
+
+implementation
+
+{ ---------------------------------------------------------------------
+  TMarkDownQuoteProcessor
+  ---------------------------------------------------------------------}
+
+function TMarkDownQuoteProcessor.LineEndsBlock(aBlock: TMarkDownContainerBlock; aLine: TMarkDownLine): Boolean;
+
+var
+  len : integer;
+  Inquote : Boolean;
+
+begin
+  Result:=(aLine=Nil);
+  if Result then
+    exit;
+  Result:=(aLine.LineNo<>aBlock.line);
+  if not Result then
+    exit;
+  // See if we have enough > markers
+  Len:=SkipQuotes(aLine,FLevel);
+  if Len=0 then
+    Exit(False);
+  inQuote:=(Len=FLevel);
+//  InQuote:=StartsWithWhiteSpace(aLine.Remainder,'>',len);
+  // Not enough markers -> check continuation.
+  // end of block if:
+  // - empty line
+  // - starts a new kind of block
+  // - It is not plain text (laziness rule)
+  if aLine.isWhitespace and not InQuote then // empty line
+    Result:=True
+  else if IsBlock(aBlock, aBlock.Blocks, aLine.Remainder) then // New kind of block
+    Result:=True
+  else if not TMarkDownParser.inPara(aBlock.blocks, false) then
+    Result:=true
+  else
+    begin
+    Result:=False;
+    Parser.Lazy:=true;
+    end;
+end;
+
+function TMarkDownQuoteProcessor.SkipQuotes(aLine : TMarkDownLine; aLevel : Integer) : integer;
+
+var
+  len,lLevel : integer;
+begin
+  // eat > markers up to current level
+  aLine.Mark;
+  lLevel:=aLevel;
+  While (lLevel>0) and StartsWithWhitespace(aLine.Remainder, '>', len) do
+    begin
+    aLine.advance(len+1);
+    if not aLine.isEmpty and isWhitespaceChar(aLine.Remainder[1]) then
+      aLine.advance(1);
+    Dec(lLevel);
+    end;
+  Result:=lLevel;
+end;
+
+function TMarkDownQuoteProcessor.IsQuotedLine(aLine: TMarkDownLine; SkipLevels : Boolean): boolean;
+
+var
+  len : integer;
+
+begin
+  if SkipLevels and (FLevel>0) then
+    SkipQuotes(aLine,FLevel);
+  Result:=StartsWithWhiteSpace(aLine.Remainder,'>',len);
+  if not Result then
+    exit;
+  aLine.advance(len+1);
+  if not aLine.isEmpty and isWhitespaceChar(aLine.Remainder[1]) then
+    aLine.advance(1);
+end;
+
+
+function TMarkDownQuoteProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
+
+begin
+  Result:=IsQuotedLine(aLine,False);
+end;
+
+
+function TMarkDownQuoteProcessor.inListOrQuote: boolean;
+begin
+  Result:=true;
+end;
+
+function TMarkDownQuoteProcessor.IsRoot(aParent : TMarkDownContainerBlock): Boolean;
+var
+  lParent : TMarkDownBlock;
+begin
+  Result:=aParent is TMarkDownDocument;
+  if Result then
+    exit;
+  lParent:=aParent;
+  While lParent<>Nil do
+    begin
+    Result:=Not (lParent is TMarkDownListBlock);
+    if Result then
+      Exit;
+    lParent:=lParent.Parent;
+    end;
+end;
+
+function TMarkDownQuoteProcessor.processLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine; aContext: TMarkDownBlockProcessingContext): Boolean;
+
+var
+  oldLevel : Integer;
+  lBlock : TMarkDownQuoteBlock;
+
+begin
+  oldLevel:=Flevel;
+  inc(FLevel);
+  lBlock:=TMarkDownQuoteBlock.Create(aParent,aLine.LineNo);
+  RedoLine(false);
+  Parse(lBlock,Self);
+  FLevel:=OldLevel;
+  Result:=True;
+end;
+
+{ ---------------------------------------------------------------------
+  TMarkDownListProcessor
+  ---------------------------------------------------------------------}
+
+function TMarkDownListProcessor.inListOrQuote: boolean;
+begin
+  Result:=true;
+end;
+
+function TMarkDownListProcessor.Root: Boolean;
+begin
+  Result:=False;
+end;
+
+function TMarkDownListProcessor.LastList: TMarkDownListBlock;
+begin
+  Result:=FLastList;
+end;
+
+
+function TMarkDownListProcessor.isList(aOrdered: boolean; const aMarker: String; aIndent: integer): boolean;
+
+begin
+  Result:=Assigned(FLastList);
+  if Result then
+    begin
+    Result:=(FLastList.Ordered=aOrdered)
+            and (FLastList.Marker=aMarker)
+            and (aIndent<FLastList.lastIndent + 1);
+    end;
+end;
+
+function TMarkDownListProcessor.prepareLine(aLine: TMarkDownLine; aContext : TMarkDownBlockProcessingContext): boolean;
+
+var
+  lWhiteSpace, lCurrLineNo,lLastIndent,len : integer;
+
+begin
+  Result:=False;
+  lCurrLineNo:=aLine.LineNo;
+  lLastIndent:=LastList.LastIndent;
+  lWhiteSpace:=aLine.LeadingWhitespace;
+  if lCurrLineNo=FLastItem.Line then
+    aLine.advance(lLastIndent)
+  else if lWhitespace >= lLastIndent then
+    begin
+    len:=lWhitespace;
+    if (len>lLastIndent+1) then
+      len:=LastList.baseIndent;
+    aLine.advance(len);
+    end
+  else if (mdoGithubFlavoured in Parser.Options) and (lWhitespace>3) and (aLine.Remainder.Trim.StartsWith('-')) then
+    Result:=False
+  else if not aLine.isWhitespace and isBlock(LastList, FLastItem.blocks, aLine.Remainder, LastList.LastIndent+LastList.grace) then
+    Result:=true;
+  if not root then
+    Exit;
+  if not aLine.isWhiteSpace then
+   begin
+      FHasContent:=true;
+      if root and not Result and LastList.HasSeenEmptyLine then
+        LastList.loose:=true;
+    end
+    else if not Result  then
+    begin
+      Parser.Lazy:=true;
+      if not FHasContent then
+      begin
+        if FEmptyLine = -1 then
+          FEmptyLine:=lCurrLineNo
+        else if FEmptyLine <> CurrentLine.LineNo then
+          Result:=true;
+      end;
+      if (aContext = bpGeneral) and (lCurrLineNo<>FLastItem.Line) then
+        LastList.HasSeenEmptyLine:=true;
+    end;
+end;
+
+
+{ ---------------------------------------------------------------------
+  TMarkDownHeadingProcessor
+  ---------------------------------------------------------------------}
+
+function TMarkDownHeadingProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
+var
+  ls : string;
+  lLen,lCount : integer;
+begin
+  FLen:=0;
+  if aLine.LeadingWhitespace >= 4 then
+    Exit(false);
+  ls:=Trim(aLine.Remainder);
+  lLen:=Length(ls);
+  lCount:=CountStartChars(ls,'#');
+  Result:=(lCount>0) and ((lCount=lLen)) or ((lCount<lLen) and (ls[lCount+1] in [' ',#9]));
+  if Result then
+    FLen:=lCount;
+end;
+
+
+function TMarkDownHeadingProcessor.processLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine; aContext: TMarkDownBlockProcessingContext): Boolean;
+
+var
+  lBlock : TMarkDownHeadingBlock;
+  lLen : integer;
+  s : String;
+
+begin
+  lBlock:=TMarkDownHeadingBlock.Create(aParent,aLine.LineNo,Flen);
+  aLine.Advance(Flen);
+  aLine.SkipWhiteSpace;
+  s:=Trim(aLine.Remainder);
+  if not isWhitespace(s) then
+    begin
+    lLen:=length(s);
+    while (lLen>0) and (s[llen]='#') do
+      Dec(lLen);
+    if (lLen = 0) then
+      s:=''
+    else if (s[lLen]=' ') then
+      s:=Copy(s,1,lLen-1);
+    end;
+  Parser.parseInline(lBlock,S);
+  Result:=True;
+end;
+
+{ ---------------------------------------------------------------------
+  TUListProcessor
+  ---------------------------------------------------------------------}
+
+function TUListProcessor.LineEndsBlock(aBlock: TMarkDownContainerBlock; aLine: TMarkDownLine): Boolean;
+var
+  lBlock : TMarkDownContainerBlock;
+  lList : TMarkDownListBlock absolute lBlock;
+
+begin
+  lBlock:=aBlock;
+  Result:=aBlock.line<>aLine.LineNo;
+  if Not Result then
+    Exit;
+  if (lBlock is TMarkDownListItemBlock) and (lBlock.Parent is TMarkDownListBlock) then
+     lBlock:=lBlock.Parent as TMarkDownListBlock;
+  if (lBlock is TMarkDownListBlock) then
+    if aLine.LeadingWhitespace>=lList.baseIndent then
+      Result:=False
+end;
+
+function TUListProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
+
+var
+  ws,i, i2 : integer;
+  s : String;
+  list : TMarkDownListBlock;
+
+begin
+  Result:=False;
+  if aLine.isEmpty then
+    Exit;
+  ws:=aLine.LeadingWhitespace;
+  if ws>3 then
+    begin
+    FMarker:=CopySkipped(aLine.Remainder,[' ',#9]);
+    if (FMarker='') or not inList(aParent.blocks, false, FMarker[1], ws, 1, list) then
+      Exit;
+    end;
+  i:=aLine.LeadingWhitespace;
+  s:=aLine.Remainder;
+  if (Length(s)<1+i) then
+    Exit;
+  if not CharInSet(s[1+i],['+','-','*']) then
+    Exit;
+  FMarker:=s[1+i];
+  s:=Copy(S,2+i,Length(S)-i);
+  if isWhitespace(s) and Parser.inPara(aParent.blocks, false) then
+    Exit;
+  if isWhitespace(s) then // nothing after if it's the only thing on the aLine
+    i2:=1
+  else
+    begin
+    i2:=LeadingWhitespace(s);
+    if (i2 = 0) then
+      Exit;
+    if (i2 >= 5) then
+      i2:=1;
+    end;
+  FIndent:=i+i2+1;
+  Result:=True;
+end;
+
+function TUListProcessor.IsItemInList(aList : TMarkDownListBlock; aLine : TMarkDownLine) : boolean;
+
+var
+  len : integer;
+
+begin
+  Result:=Assigned(aLine);
+  if Not Result then
+    exit;
+  Result:=StartsWithWhitespace(aLine.line,aList.marker[1],len,aList.baseIndent);
+end;
+
+function TUListProcessor.processLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine; aContext: TMarkDownBlockProcessingContext): boolean;
+
+var
+  lOldLastList, lList : TMarkDownListBlock;
+  lOldLastItem, lItem : TMarkDownListItemBlock;
+  lMarker : string;
+  lNewItem : Boolean;
+
+begin
+  lOldLastList:=FLastList;
+  lOldLastItem:=FLastItem;
+  lMarker:=FMarker;
+  if InList(aParent.blocks,False,lMarker,FIndent,1,lList) then
+    lList.Lastindent:=FIndent
+  else
+    begin
+    lList:=TMarkDownListBlock.Create(aParent,aLine.LineNo);
+    lList.Ordered:=false;
+    lList.BaseIndent:=FIndent;
+    lList.LastIndent:=lList.BaseIndent;
+    lList.Marker:=lMarker;
+    FLastList:=lList;
+    end;
+  // While we have a list item part of this list block, add an item and parse it.
+  repeat
+    lItem:=TMarkDownListItemBlock.Create(lList,aLine.LineNo);
+    FLastItem:=lItem;
+    PrepareLine(aLine,bpGeneral);
+    RedoLine(False);
+    Parse(lItem,Self);
+    lNewItem:=Not Done;
+    if lNewItem then
+      begin
+      aLine:=NextLine;
+      lNewItem:=IsItemInList(lList,aLine);
+      end;
+    lItem.Closed:=true;
+  until not lNewItem;
+  FLastItem:=lOldLastItem;
+  FLastList:=lOldLastList;
+  Result:=True;
+end;
+
+
+{ ---------------------------------------------------------------------
+  TOListProcessor
+  ---------------------------------------------------------------------}
+
+function TOListProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
+
+var
+  list : TMarkDownListBlock;
+  lMarkerLen,lIndent, i2 : integer;
+  lMarker,lLine : String;
+
+begin
+  Result:=false;
+  if aLine.isEmpty then
+    Exit;
+  if (aLine.LeadingWhitespace >= 4) then
+    begin
+    FMarker:=CopySkipped(aLine.Remainder, [' ']);
+    FMarker:=CopySkipped(FMarker, ['0'..'9']);
+    if (FMarker = '') or not inList(aParent.blocks, true, FMarker[1], aLine.LeadingWhitespace, 2, list) then
+      Exit;
+    end;
+  lIndent:=aLine.LeadingWhitespace;
+  aLine.mark;
+  try
+    aLine.SkipWhiteSpace;
+    lLine:=aLine.Remainder;
+    lMarker:=CopyMatching(lLine, ['0'..'9']);
+    lMarkerLen:=length(lMarker)+1; // ending dot or )
+    if (lMarkerLen=1) or (lMarkerLen>10) or (lMarkerLen>Length(lLine)) then
+      Exit;
+    if not CharInSet(lLine[lMarkerLen], ['.', ')']) then
+      Exit;
+    // rule 267
+    if Parser.inPara(aParent.blocks, false) and (lMarker<>'1') then
+      Exit;
+    FMarker:=lLine[lMarkerLen];
+    FStart:=StrToIntDef(lMarker,1);
+    inc(lIndent,lMarkerLen);
+    lLine:=Copy(lLine,lMarkerLen+1,Length(lLine)-lMarkerLen);
+    if isWhitespace(lLine) and Parser.inPara(aParent.blocks, false) then
+      Exit;
+    Result:=true;
+  finally
+    if not Result then
+      aLine.rewind;
+  end;
+  // Calculate indent to use...
+  if isWhitespace(lLine) then
+    i2:=1
+  else
+    begin
+    i2:=LeadingWhitespace(lLine);
+    if (i2 = 0) then
+      Exit(false);
+    if (i2 >= 5) then
+      i2:=1;
+    end;
+  FIndent:=lIndent+i2;
+end;
+
+function TOListProcessor.IsItemInList(aList : TMarkDownListBlock; aLine : TMarkDownLine) : boolean;
+
+var
+  len : integer;
+  lLine : string;
+
+begin
+  Result:=Assigned(aLine);
+  if Not Result then
+    exit;
+  Result:=StartsWithWhitespace(aLine.Line,['0'..'9'],len,aList.baseIndent);
+  if Not Result then
+    exit;
+  if len<=aList.baseIndent then
+    begin
+    lLine:=aLine.Line;
+    if Len>0 then
+      Delete(lLine,1,Len);
+    lLine:=CopySkipped(lLine,['0'..'9']);
+    Result:=(lLine<>'') and (aList.marker=lLine[1]);
+    if Result then
+      Result:=(Length(lLine)>1) and (lLine[2]=' ');
+    end;
+end;
+
+function TOListProcessor.LineEndsBlock(aBlock: TMarkDownContainerBlock; aLine: TMarkDownLine): Boolean;
+var
+  lBlock : TMarkDownContainerBlock;
+  lList : TMarkDownListBlock absolute lBlock;
+
+begin
+  lBlock:=aBlock;
+  Result:=aBlock.line<>aLine.LineNo;
+  if Not Result then
+    Exit;
+  if (lBlock is TMarkDownListItemBlock) and (lBlock.Parent is TMarkDownListBlock) then
+     lBlock:=lBlock.Parent as TMarkDownListBlock;
+  if (lBlock is TMarkDownListBlock) then
+    if aLine.LeadingWhitespace>=lList.baseIndent then
+      Result:=False
+end;
+
+
+function TOListProcessor.processLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine; aContext: TMarkDownBlockProcessingContext): boolean;
+var
+  lOldLastList, lList : TMarkDownListBlock;
+  lOldLastItem, lItem : TMarkDownListItemBlock;
+  lNewItem : Boolean;
+begin
+  lOldLastList:=FLastList;
+  if inList(aParent.blocks, true, FMarker, Findent, 2, lList) then
+    lList.lastIndent:=FIndent
+  else
+    begin
+    lList:=TMarkDownListBlock.Create(aParent,aLine.LineNo);
+    lList.ordered:=true;
+    lList.baseIndent:=Findent;
+    lList.lastIndent:=lList.baseIndent;
+    lList.marker:=FMarker;
+    lList.Start:=FStart;
+    FLastList:=lList;
+    end;
+  // While we have a line that part of this list block, add an item and parse it.
+  repeat
+    lItem:=TMarkDownListItemBlock.Create(lList,aLine.LineNo);
+    lOldLastItem:=FLastItem;
+    FLastItem:=lItem;
+    PrepareLine(aLine,bpGeneral);
+    RedoLine(False);
+    Parse(lItem,Self);
+    lNewItem:=Not Done;
+    if lNewItem then
+      begin
+      aLine:=NextLine;
+      lNewItem:=IsItemInList(lList,aLine);
+      end;
+  until not lNewItem;
+  FLastItem:=lOldLastItem;
+  FLastList:=lOldLastList;
+  Result:=True;
+end;
+
+{ ---------------------------------------------------------------------
+  TCodeBlockProcessor
+  ---------------------------------------------------------------------}
+
+function TCodeBlockProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
+
+var
+  Indent : integer;
+
+begin
+  Result:=False;
+  Indent:=aLine.LeadingWhitespace;
+  if Indent < 4 then
+    Exit;
+  if aLine.isWhitespace then
+    Exit;
+  if Parser.inPara(aParent.blocks,True) then
+    Exit;
+  Result:=true;
+end;
+
+procedure TCodeBlockProcessor.ProcessCodeBlock(C: TMarkDownCodeBlock; aLine: TMarkDownLine);
+
+var
+  S : String;
+  lContinue : boolean;
+  lIndent : Integer;
+begin
+  lIndent:=aLine.LeadingWhitespace;
+  aLine.advance(lIndent);
+  S:=aLine.Remainder;
+  lContinue:=True;
+  While lContinue do
+    begin
+    TMarkDownTextBlock.Create(C,aLine.LineNo,S);
+    aLine:=peekLine;
+    if (aLine = nil) then
+      Exit;
+    if not Handlesline(C,aLine) then
+      Exit;
+    S:=aLine.Remainder;
+    if lengthWhitespaceCorrected(S)<=lIndent then
+      lContinue:=isWhitespace(S)
+    else
+      lContinue:=LeadingWhitespace(S)>=lIndent;
+    if lContinue then
+      begin
+      // Actually get the line
+      aLine:=NextLine;
+      aLine.advance(lIndent);
+      S:=aLine.Remainder;
+      end;
+    end;
+end;
+
+function TCodeBlockProcessor.processLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine; aContext: TMarkDownBlockProcessingContext): Boolean;
+
+var
+  lBlock : TMarkDownCodeBlock;
+
+begin
+  lBlock:=TMarkDownCodeBlock.Create(aParent,aLine.LineNo);
+  ProcessCodeBlock(lBlock,aLine);
+  // No whitespace at the end
+  while (lBlock.ChildCount>0) and isWhitespace((lBlock.LastChild as TMarkDownTextBlock).text) do
+    lBlock.DeleteChild(lBlock.ChildCount-1);
+  Result:=true;
+end;
+
+
+function TFencedCodeBlockProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
+
+var
+  s, s1 : String;
+  c : char;
+  len, i, j : integer;
+begin
+  Result:=False;
+  FLang:='';
+  if aLine.LeadingWhitespace >= 4 then
+    Exit;
+  s:=aLine.Remainder.Trim;
+  if (S='') then
+    Exit;
+  c:=S[1];
+  if not (C in ['`','~']) then
+    Exit;
+  len:=CountStartChars(S,c);
+  if (len<3) or (Pos(c,S,len+2)<>0) then
+    Exit;
+  FTerminal:=Copy(s,1,Len);
+  Result:=true;
+  // now, try to find anything off the end of the fence
+  s:=aLine.Remainder;
+  Findent:=1;
+  while s[FIndent] = ' ' do
+    inc(FIndent);
+  if FIndent>1 then
+    Delete(s,1,FIndent-1);
+  S:=CopySkipped(S,[FTerminal[1]]).trim;
+  i:=1;
+  while (i<=Length(s)) do
+    begin
+    if S[i] = '\' then
+      delete(s, i, 1) // and omit checking the next character
+    else if s[i] = '`' then
+      Exit(false)
+    else if s[i] = '&' then
+    begin
+      j:=i+1;
+      while (j <= Length(s)) and (s[j] <> ';') do
+        inc(j);
+      if j <= length(s) then
+      begin
+        s1:=parseEntityString(Parser.Entities,copy(s, i, j-i+1));
+        delete(s, i, j-1);
+        insert(s1, s, i);
+        inc(i, Length(s1)-1);
+      end;
+    end;
+    inc(i);
+  end;
+
+  if (s <> '') then
+  begin
+    Flang:=CopyUpTo(s, [' ']);
+    if Flang.contains(FTerminal[1]) then
+      Exit(false);
+  end;
+end;
+
+function TFencedCodeBlockProcessor.LineEndsBlock(aBlock: TMarkDownContainerBlock; aLine: TMarkDownLine): Boolean;
+
+var
+  s : String;
+
+begin
+  Result:=(aLine=nil);
+  if Result then
+    Exit;
+  // Ending may be preceded by 3 spaces
+  Result:=aLine.LeadingWhitespace>=4;
+  if Result then
+    Exit;
+  S:=aLine.Remainder.Trim;
+  Result:=IsStringOfChar(s) and s.StartsWith(FTerminal);
+end;
+
+function TFencedCodeBlockProcessor.processLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine; aContext: TMarkDownBlockProcessingContext): Boolean;
+
+var
+  lBlock : TMarkDownCodeBlock;
+  s : String;
+  i : integer;
+
+begin
+  lBlock:=TMarkDownCodeBlock.Create(aParent,aLine.LineNo);
+  lBlock.fenced:=true;
+  lBlock.lang:=Flang;
+  while Not LineEndsBlock(lBlock,PeekLine) do
+    begin
+    aLine:=NextLine;
+    s:=aLine.Remainder;
+    if (FIndent>0) then
+      begin
+      if FIndent>Length(S) then
+        FIndent:=Length(S);
+      I:=1;
+      while (I<=Findent) and (s[i]=' ') do
+        Inc(i);
+      if I>1 then
+        Delete(S,1,I-1);
+      aLine.advance(I-1);
+      end;
+    TMarkDownTextBlock.Create(lBlock,aLine.LineNo,S);
+    end;
+  NextLine;
+  Result:=true;
+end;
+
+function TTableProcessor.CountCells(aLine: TMarkDownLine): Integer;
+
+var
+  c : char;
+  lEscaped: boolean;
+  S : String;
+
+begin
+  Result:=0;
+  lEscaped:=false;
+  for c in aLine.Line do
+    begin
+    if not lEscaped and (c = '|') then
+      inc(Result)
+    else if lEscaped then
+      lEscaped:=false
+    else if c = '\' then
+      lEscaped:=true;
+    end;
+  if Result=0 then
+    exit;
+  S:=Trim(aLine.Line);
+  if (S<>'') then
+    begin
+    if not StartsStr('|',S) then
+      inc(Result);
+    if not EndsStr('|',S) or EndsStr('\|',S) then
+      inc(Result);
+    end;
+end;
+
+function TTableProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
+
+var
+  lCount,lCountNext  : integer;
+  lNextLine : TMarkDownLine;
+  lLine : string;
+  c : char;
+
+begin
+  Result:=False;
+  if aLine.isEmpty then
+    exit;
+  lCount:=CountCells(aLine);
+  if lCount=0 then
+    Exit;
+  lNextLine:=peekLine;
+  if (lNextLine=nil) then
+    Exit;
+  lLine:=lNextLine.Line;
+  for c in lLine do
+    if not CharInSet(c,[' ','|','-',':']) then
+      Exit(false);
+  Result:=not Strict;
+  lCountNext:=CountCells(lNextLine);
+  // We have a table if the number of cells is equal
+  Result:=(lCount=lCountNext)
+end;
+
+function TTableProcessor.processLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine; aContext: TMarkDownBlockProcessingContext): Boolean;
+
+var
+  i : integer;
+  lLine,lCell : String;
+  lTable : TMarkDownTableBlock;
+  lCells : TStringDynArray;
+  lColumns : TCellAlignArray;
+
+begin
+  LColumns:=[];
+  lTable:=TMarkDownTableBlock.Create(aParent,aLine.LineNo);
+  parseTableLine(lTable, aLine);
+  lLine:=Trim(NextLine.Line);
+  if StartsStr('|',lLine) then
+    lLine:=Copy(lLine,2,Length(lLine)-1);
+  if EndsStr('|',lLine) then
+    SetLength(lLine,Length(lLine)-1);
+  lCells:=lLine.Split(['|']);
+  SetLength(LColumns, length(lCells));
+  for i:=0 to length(lCells) - 1 do
+    begin
+    lColumns[i]:=caLeft;
+    lCell:=Trim(lCells[i]);
+    if StartsStr(':',lCell) and EndsStr(':',lCell) then
+      lColumns[i]:=caCenter
+    else if EndsStr(':',lCell) then
+      lColumns[i]:=caRight
+    end;
+  lTable.Columns:=lColumns;
+  while not isEndOfTable(peekLine) do
+    ParseTableLine(lTable,NextLine);
+  Result:=True;
+end;
+
+procedure TTableProcessor.ParseTableLine(aTable: TMarkDownTableBlock; aLine: TMarkDownLine);
+
+var
+  lRow : TMarkDownTableRowBlock;
+  lLen,lStart, i : integer;
+  lEscaped : boolean;
+  lLine : string;
+  lChar : char;
+
+  procedure AddCell(aStart,aEnd : integer);
+
+  var
+    lCell : String;
+
+  begin
+    lCell:=copy(aLine.Line, aStart, aEnd-aStart);
+    lCell:=StringReplace(Trim(lCell),'\|', '|',[rfReplaceAll]);
+    TMarkDownTextBlock.Create(lRow,aLine.LineNo,lCell);
+  end;
+
+begin
+  lRow:=TMarkDownTableRowBlock.Create(aTable,aLine.LineNo);
+  i:=1;
+  lStart:=1;
+  lEscaped:=false;
+  lLine:=aLine.Line;
+  lLen:=Length(lLine);
+  while i<=lLen do
+    begin
+    lChar:=lLine[i];
+    if (i=1) and (lChar='|') then
+      lStart:=2
+    else if lEscaped then
+      lEscaped:=False
+    else if lChar = '\' then
+      lEscaped:=True
+    else if lChar = '|' then
+      begin
+      AddCell(lStart,i);
+      lStart:=i+1;
+      end;
+    Inc(i);
+    end;
+  if (lStart<i) then
+    addCell(lStart,i);
+end;
+
+function TTableProcessor.IsEndOfTable(aLine: TMarkDownLine): boolean;
+
+var
+  S : String;
+  lLen : integer;
+
+begin
+  if aLine=nil then
+    Exit(true);
+  S:=aLine.Line;
+  lLen:=Length(S);
+  Result:=(Trim(S) = '')
+          or ((lLen>0) and (S[1] in ['#','~']))
+          or StartsStr('   ',S);
+end;
+
+function TTableProcessor.Strict: boolean;
+begin
+  Result:=DefaultStrict;
+end;
+
+{ ---------------------------------------------------------------------
+  TParagraphProcessor
+  ---------------------------------------------------------------------}
+
+function TParagraphProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
+begin
+  Result:=True;
+end;
+
+function TParagraphProcessor.processLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine; aContext: TMarkDownBlockProcessingContext): boolean;
+
+var
+  lPar : TMarkDownParagraphBlock;
+
+begin
+  Result:=true;
+  if Parser.inPara(aParent.blocks, true) then
+    lPar:=aParent.Blocks.Last as TMarkDownParagraphBlock
+  else
+    lPar:=nil;
+  if aLine.isEmpty or aLine.isWhitespace then
+    begin
+    if Assigned(lPar) then
+      lPar.closed:=true;
+    end
+  else
+    begin
+    if Not Assigned(lPar) then
+      lPar:=TMarkDownParagraphBlock.Create(aParent,aLine.LineNo);
+    Parser.parseInLine(lPar,aLine.Remainder);
+    end;
+end;
+
+{ ---------------------------------------------------------------------
+  TSeTextHeaderProcessor
+  ---------------------------------------------------------------------}
+
+function TSeTextHeaderProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
+
+var
+  p : TMarkDownParagraphBlock;
+  s : String;
+
+begin
+  Result:=False;
+  if aParent.blocks.Count=0 then
+    Exit;
+  if aLine.LeadingWhitespace>=4 then
+    Exit;
+  if not TMarkDownParser.inPara(aParent.blocks,False) then
+    Exit;
+  p:=aParent.blocks.Last as TMarkDownParagraphBlock;
+  if p.closed then
+    Exit;
+  if p.header<>0 then
+    Exit;
+  if Parser.Lazy and inListOrQuote then
+    Exit;
+  s:=aLine.Remainder.Trim;
+  if (s='') then
+    Exit;
+  if not IsStringOfChar(s) then
+    Exit;
+  Result:=True;
+end;
+
+
+function TSeTextHeaderProcessor.processLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine; aContext: TMarkDownBlockProcessingContext): boolean;
+
+var
+  S : String;
+  h : integer;
+  P : TMarkDownParagraphBlock;
+
+begin
+  s:=aLine.Remainder.Trim;
+  h:=Pos(S[1],'=-');
+  Result:=h>0;
+  if Result then
+    begin
+    p:=aParent.blocks.Last as TMarkDownParagraphBlock;
+    p.header:=h;
+    end;
+end;
+
+{ ---------------------------------------------------------------------
+  TThematicBreakProcessor
+  ---------------------------------------------------------------------}
+
+function TThematicBreakProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
+
+var
+  S : String;
+  C : char;
+
+begin
+  Result:=False;
+  if aLine.LeadingWhitespace >= 4 then
+    Exit;
+  S:=StripWhitespace(aLine.Remainder);
+  if (S='') then
+    Exit;
+  C:=S[1];
+  Result:=(C in ['*','-','_']) and (CountStartChars(S,C)>2) and IsStringOfChar(S);
+end;
+
+
+function TThematicBreakProcessor.processLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine; aContext: TMarkDownBlockProcessingContext): Boolean;
+
+begin
+  TMarkDownThematicBreakBlock.Create(aParent,aLine.LineNo);
+  Result:=true;
+end;
+
+Procedure RegisterDefaultProcessors;
+
+begin
+  TMarkDownQuoteProcessor.Register('quote');
+  // Must be registered before thematic break
+  TSeTextHeaderProcessor.register('setextheader');
+  TThematicBreakProcessor.Register('break');
+  TCodeBlockProcessor.register('code');
+  TFencedCodeBlockProcessor.register('fencedcode');
+  TMarkDownHeadingProcessor.register('heading');
+  TUListProcessor.Register('unorderedlist');
+  TOListProcessor.Register('orderedlist');
+  TTableProcessor.Register('table');
+  TParagraphProcessor.Register('paragraph');
+end;
+
+initialization
+  TTableProcessor.DefaultStrict:=True;
+  RegisterDefaultProcessors;
+end.
+

+ 513 - 0
packages/fcl-md/src/markdown.render.pas

@@ -0,0 +1,513 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown renderer class & render factory.
+
+    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.Render;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+{$IFDEF FPC_DOTTEDUNITS}
+  System.Classes, System.SysUtils, System.Contnrs,
+{$ELSE}  
+  Classes, SysUtils, Contnrs,
+{$ENDIF}
+  MarkDown.Elements,
+  MarkDown.Utils;
+
+Type
+  TMarkDownBlockRenderer = class;
+  TMarkDownBlockRendererClass = class of TMarkDownBlockRenderer;
+  TMarkDownTextRenderer = class;
+  TMarkDownTextRendererClass = class of TMarkDownTextRenderer;
+
+
+  { TMarkDownRenderer }
+
+  TMarkDownRenderer = class(TComponent)
+  private
+    FSkipUnknownElements: Boolean;
+    FTextRenderer : TMarkDownTextRenderer;
+  protected
+    function CreateRendererInstance(aClass : TMarkDownBlockRendererClass) : TMarkDownBlockRenderer; virtual;
+    function CreateRendererForBlock(aBlock : TMarkdownBlock) : TMarkDownBlockRenderer; virtual;
+    function CreateTextRendererInstance(aClass : TMarkDownTextRendererClass): TMarkDownTextRenderer; virtual;
+    function GetTextRenderer : TMarkDownTextRenderer;
+  public
+    destructor destroy; override;
+    Procedure RenderText(aText : TMarkDownTextNode); virtual;
+    Procedure RenderTextNodes(aTextNodes : TMarkDownTextNodeList);
+    Procedure RenderBlock(aBlock : TMarkdownBlock); virtual;
+    procedure RenderCodeBlock(aBlock: TMarkdownBlock; const aLang: string); virtual;
+    procedure RenderChildren(aBlock : TMarkDownContainerBlock); virtual;
+    Procedure RenderDocument(aDocument : TMarkDownDocument); virtual; abstract;
+  published
+    Property SkipUnknownElements : Boolean read FSkipUnknownElements Write FSkipUnknownElements;
+  end;
+  TMarkDownRendererClass = class of TMarkDownRenderer;
+
+  { TMarkDownElementRenderer }
+
+  TMarkDownElementRenderer = Class (TObject)
+  private
+    FRenderer: TMarkDownRenderer;
+  protected
+    Property Renderer : TMarkDownRenderer read FRenderer;
+  public
+    constructor create(aRenderer : TMarkDownRenderer);
+    procedure reset; virtual;
+  end;
+  TMarkDownElementRendererClass = class of TMarkDownElementRenderer;
+
+  { TMarkDownBlockRenderer }
+
+  TMarkDownBlockRenderer = Class (TMarkDownElementRenderer)
+  protected
+    procedure DoRender(aBlock: TMarkDownBlock); virtual; abstract;
+  Public
+    class function BlockClass : TMarkDownBlockClass; virtual; abstract;
+    class procedure RegisterRenderer(aRendererClass : TMarkDownRendererClass);
+    procedure Render(aBlock : TMarkDownBlock); inline;
+  end;
+
+  { TMarkDownTextRenderer }
+
+  TMarkDownTextRenderer = class(TMarkDownElementRenderer)
+  protected
+    procedure DoRender(aElement: TMarkDownTextNode); virtual; abstract;
+  public
+    class procedure RegisterRenderer(aRendererClass : TMarkDownRendererClass);
+    procedure render(aElement : TMarkDownTextNode); inline;
+    // Block state management.
+    procedure BeginBlock; virtual;
+    procedure EndBlock; virtual;
+  end;
+
+  { TNullRenderer }
+
+  TNullRenderer = class(TMarkDownBlockRenderer)
+  protected
+    procedure DoRender(aBlock: TMarkDownBlock); override;
+  end;
+
+  { TDocumentRenderer }
+
+  TDocumentRenderer = class(TMarkDownBlockRenderer)
+    class function BlockClass : TMarkDownBlockClass; override;
+  end;
+
+
+  { TMarkDownRendererFactory }
+
+  TMarkDownRendererFactory = class(TObject)
+  private
+    class var _Instance : TMarkDownRendererFactory;
+  type
+    { TBlockRenderRegistration }
+
+    TBlockRenderRegistration = class
+      BlockClass : TMarkDownBlockClass;
+      RendererClass : TMarkDownBlockRendererClass;
+      constructor create(aBlockClass : TMarkDownBlockClass; aRendererClass : TMarkDownBlockRendererClass);
+    end;
+    TBlockRenderRegistrationList = Specialize TGFPObjectList<TBlockRenderRegistration>;
+
+    { TRenderBlockRenderers }
+
+    TRenderBlockRenderers = class(TObject)
+      Renderer : TMarkDownRendererClass;
+      BlockRenderers : TBlockRenderRegistrationList;
+      Textrenderer : TMarkDownTextRendererClass;
+      Constructor create(aRenderer : TMarkDownRendererClass);
+      destructor destroy; override;
+      function FindBlock (aClass : TMarkdownBlockClass; aAllowCreate : Boolean) : TBlockRenderRegistration;
+     end;
+     TRenderBlockRenderersList = specialize TGFPObjectList<TRenderBlockRenderers>;
+
+  private
+    FRegistry : TRenderBlockRenderersList;
+  protected
+    function FindRenderer(aClass : TMarkDownRendererClass; allowCreate : Boolean) : TRenderBlockRenderers;
+  public
+    class constructor init;
+    class destructor done;
+    constructor create;
+    destructor destroy; override;
+    procedure RegisterBlockRenderer(aRendererClass : TMarkdownRendererClass; aBlockClass : TMarkdownBlockClass; aBlockRendererClass : TMarkdownBlockRendererClass);
+    function FindBlockRendererClass(aRendererClass : TMarkdownRendererClass; aBlockClass : TMarkdownBlockClass) : TMarkdownBlockRendererClass;
+    procedure RegisterTextRenderer(aRendererClass : TMarkdownRendererClass; aTextRendererClass : TMarkdownTextRendererClass);
+    function FindTextRendererClass(aRendererClass : TMarkdownRendererClass) : TMarkdownTextRendererClass;
+    class property Instance : TMarkDownRendererFactory read _Instance;
+  end;
+
+implementation
+
+class procedure TMarkDownBlockRenderer.RegisterRenderer(aRendererClass: TMarkDownRendererClass);
+
+begin
+  TMarkDownRendererFactory.Instance.RegisterBlockRenderer(aRendererClass, BlockClass, Self);
+end;
+
+
+procedure TMarkDownBlockRenderer.render(aBlock: TMarkDownBlock);
+
+begin
+  DoRender(aBlock);
+end;
+
+
+{ TMarkDownTextRenderer }
+
+class procedure TMarkDownTextRenderer.RegisterRenderer(aRendererClass: TMarkDownRendererClass);
+
+begin
+  TMarkDownRendererFactory.Instance.RegisterTextRenderer(aRendererClass,Self);
+end;
+
+
+procedure TMarkDownTextRenderer.render(aElement: TMarkDownTextNode);
+
+begin
+  DoRender(aElement);
+end;
+
+
+procedure TMarkDownTextRenderer.BeginBlock;
+
+begin
+  // Do nothing
+end;
+
+procedure TMarkDownTextRenderer.EndBlock;
+
+begin
+  //
+end;
+
+{ TNullRenderer }
+
+procedure TNullRenderer.DoRender(aBlock: TMarkDownBlock);
+
+begin
+  if aBlock is TMarkDownContainerBlock then
+    Renderer.RenderChildren(TMarkDownContainerBlock(aBlock));
+end;
+
+
+{ TDocumentRenderer }
+
+class function TDocumentRenderer.BlockClass: TMarkDownBlockClass;
+
+begin
+  Result:=TMarkDownDocument;
+end;
+
+
+function TMarkDownRenderer.CreateRendererInstance(aClass: TMarkDownBlockRendererClass): TMarkDownBlockRenderer;
+
+begin
+  Result:=aClass.Create(Self);
+end;
+
+
+function TMarkDownRenderer.CreateRendererForBlock(aBlock: TMarkdownBlock): TMarkDownBlockRenderer;
+
+var
+  lRenderClass : TMarkDownRendererClass;
+  lBlockClass : TMarkDownBlockClass;
+  LBlockRendererClass : TMarkDownBlockRendererClass;
+
+begin
+  Result:=Nil;
+  lRenderClass:=TMarkDownRendererClass(Self.ClassType);
+  lBlockClass:=TMarkDownBlockClass(aBlock.ClassType);
+  LBlockRendererClass:=TMarkDownRendererFactory.Instance.FindBlockRendererClass(lRenderClass,lBlockClass);
+  if assigned(LBlockRendererClass) then
+    Result:=CreateRendererInstance(LBlockRendererClass)
+end;
+
+
+function TMarkDownRenderer.CreateTextRendererInstance(aClass : TMarkDownTextRendererClass): TMarkDownTextRenderer;
+
+begin
+  Result:=aClass.Create(Self);
+end;
+
+function TMarkDownRenderer.GetTextRenderer: TMarkDownTextRenderer;
+
+var
+  lClass : TMarkDownTextRendererClass;
+  lRenderClass : TMarkDownRendererClass;
+
+begin
+  Result:=nil;
+  if FTextRenderer=Nil then;
+    begin
+    lRenderClass:=TMarkDownRendererClass(Self.ClassType);
+    lClass:=TMarkDownRendererFactory.Instance.FindTextRendererClass(lRenderClass);
+    if assigned(lClass) then
+      FTextRenderer:=CreateTextRendererInstance(lClass);
+    end;
+  Result:=FTextRenderer;
+end;
+
+destructor TMarkDownRenderer.destroy;
+begin
+  FreeAndNil(FTextRenderer);
+  inherited destroy;
+end;
+
+
+procedure TMarkDownRenderer.RenderText(aText: TMarkDownTextNode);
+
+var
+  lRender : TMarkDownTextRenderer;
+
+begin
+  lRender:=GetTextRenderer;
+  lRender.BeginBlock;
+  lRender.render(aText);
+  lRender.EndBlock;
+end;
+
+procedure TMarkDownRenderer.RenderTextNodes(aTextNodes: TMarkDownTextNodeList);
+
+var
+  lRender : TMarkDownTextRenderer;
+  lNode : TMarkDownTextNode;
+
+begin
+  lRender:=GetTextRenderer;
+  lRender.BeginBlock;
+  For lNode in aTextNodes do
+    lRender.render(lNode);
+  lRender.EndBlock;
+end;
+
+procedure TMarkDownRenderer.RenderBlock(aBlock: TMarkdownBlock);
+
+var
+  lRender : TMarkDownBlockRenderer;
+
+begin
+  if aBlock=Nil then
+    Raise EMarkdown.Create('Cannot render nil block');
+  lRender:=CreateRendererForBlock(aBlock);
+  try
+    if Assigned(lRender) then
+      lRender.render(aBlock)
+    else
+      Raise EMarkDown.CreateFmt('No renderer for block class: %s',[aBlock.ClassName]);
+  finally
+    lRender.Free;
+  end;
+end;
+
+procedure TMarkDownRenderer.RenderCodeBlock(aBlock: TMarkdownBlock; const aLang: string);
+begin
+  if (aLang='') then ; // Silence warning
+  RenderBlock(aBlock);
+end;
+
+procedure TMarkDownRenderer.RenderChildren(aBlock: TMarkDownContainerBlock);
+
+var
+  I : integer;
+
+begin
+  for I:=0 to aBlock.Blocks.Count-1 do
+    RenderBlock(aBlock.Blocks[I]);
+end;
+
+{ TMarkDownElementRenderer }
+
+constructor TMarkDownElementRenderer.create(aRenderer: TMarkDownRenderer);
+
+begin
+  FRenderer:=aRenderer;
+end;
+
+procedure TMarkDownElementRenderer.reset;
+
+begin
+  // Do nothing
+end;
+
+{ TMarkDownRendererFactory }
+
+constructor TMarkDownRendererFactory.create;
+
+begin
+  FRegistry:=TRenderBlockRenderersList.Create(True);
+end;
+
+destructor TMarkDownRendererFactory.destroy;
+
+begin
+  FreeAndNil(FRegistry);
+  inherited destroy;
+end;
+
+
+procedure TMarkDownRendererFactory.RegisterBlockRenderer(aRendererClass: TMarkdownRendererClass; aBlockClass: TMarkdownBlockClass;
+  aBlockRendererClass: TMarkdownBlockRendererClass);
+
+var
+  lList : TRenderBlockRenderers;
+  lReg : TBlockRenderRegistration;
+
+begin
+  lList:=FindRenderer(aRendererClass,True);
+  lReg:=lList.FindBlock(aBlockClass,True);
+  lReg.RendererClass:=aBlockRendererClass;
+end;
+
+
+function TMarkDownRendererFactory.FindBlockRendererClass(aRendererClass: TMarkdownRendererClass; aBlockClass: TMarkdownBlockClass
+  ): TMarkdownBlockRendererClass;
+
+var
+  lList : TRenderBlockRenderers;
+  lReg : TBlockRenderRegistration;
+
+begin
+  Result:=Nil;
+  lList:=FindRenderer(aRendererClass,False);
+  if Assigned(lList) then
+    begin
+    lReg:=lList.FindBlock(aBlockClass,False);
+    if assigned(lReg) then
+      Result:=lReg.RendererClass;
+    end;
+end;
+
+
+procedure TMarkDownRendererFactory.RegisterTextRenderer(aRendererClass: TMarkdownRendererClass;
+  aTextRendererClass: TMarkdownTextRendererClass);
+
+var
+  lList : TRenderBlockRenderers;
+
+begin
+  lList:=FindRenderer(aRendererClass,True);
+  lList.Textrenderer:=aTextRendererClass;
+end;
+
+
+function TMarkDownRendererFactory.FindTextRendererClass(aRendererClass: TMarkdownRendererClass): TMarkdownTextRendererClass;
+
+var
+  lList : TRenderBlockRenderers;
+
+begin
+  lList:=FindRenderer(aRendererClass,True);
+  if assigned(lList) then
+    Result:=lList.Textrenderer;
+end;
+
+
+function TMarkDownRendererFactory.FindRenderer(aClass: TMarkDownRendererClass; allowCreate: Boolean): TRenderBlockRenderers;
+
+var
+  I : Integer;
+
+begin
+  Result:=Nil;
+  I:=0;
+  While (Result=Nil) and (I<FRegistry.Count) do
+    begin
+    Result:=FRegistry[I];
+    if Result.Renderer<>aClass then
+      Result:=Nil;
+    Inc(I);
+    end;
+  if (Result=nil) and AllowCreate then
+    begin
+    Result:=TRenderBlockRenderers.Create(aClass);
+    FRegistry.Add(Result);
+    end;
+end;
+
+
+class constructor TMarkDownRendererFactory.init;
+
+begin
+  _Instance:=TMarkDownRendererFactory.Create;
+end;
+
+
+class destructor TMarkDownRendererFactory.done;
+
+begin
+  FreeAndNil(_Instance);
+end;
+
+
+{ TMarkDownRendererFactory.TBlockRenderRegistration }
+
+constructor TMarkDownRendererFactory.TBlockRenderRegistration.create(aBlockClass: TMarkDownBlockClass;
+  aRendererClass: TMarkDownBlockRendererClass);
+
+begin
+  BlockClass:=aBlockClass;
+  RendererClass:=aRendererClass;
+end;
+
+
+{ TMarkDownRendererFactory.TRenderBlockRenderers }
+
+constructor TMarkDownRendererFactory.TRenderBlockRenderers.create(aRenderer: TMarkDownRendererClass);
+
+begin
+  Renderer:=aRenderer;
+  BlockRenderers:=TBlockRenderRegistrationList.Create(True);
+end;
+
+
+destructor TMarkDownRendererFactory.TRenderBlockRenderers.destroy;
+
+begin
+  FreeAndNil(BlockRenderers);
+  inherited destroy;
+end;
+
+
+function TMarkDownRendererFactory.TRenderBlockRenderers.FindBlock(aClass: TMarkdownBlockClass; aAllowCreate: Boolean
+  ): TBlockRenderRegistration;
+
+var
+  I : Integer;
+
+begin
+  Result:=Nil;
+  I:=0;
+  While (Result=Nil) and (I<BlockRenderers.Count) do
+    begin
+    Result:=BlockRenderers[I];
+    if Result.BlockClass<>aClass then
+      Result:=Nil;
+    Inc(I);
+    end;
+  if (Result=nil) and aAllowCreate then
+    begin
+    Result:=TBlockRenderRegistration.Create(aClass,Nil);
+    BlockRenderers.Add(Result);
+    end;
+end;
+
+
+end.
+

+ 348 - 0
packages/fcl-md/src/markdown.scanner.pas

@@ -0,0 +1,348 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown text scanner.
+
+    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.Scanner;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+{$IFDEF FPC_DOTTEDUNITS}
+  System.SysUtils, 
+{$ELSE}  
+  SysUtils, 
+{$ENDIF}
+  MarkDown.Elements;
+
+type
+
+  { TMarkDownTextScanner }
+
+  TMarkDownTextScanner = class
+  private
+    FText : String;
+    FCursor : integer;
+    FMark  : Integer;
+    FMarkPos : TPosition;
+    FPos : TPosition;
+    FLineNo : integer;
+    function GetEOF: boolean;
+    function GetPeek: char;
+    function GetPeekNext: char;
+    function GetPeekLast: char;
+    function GetPeekEndRun: char;
+  Protected
+    Property Cursor : Integer Read FCursor Write FCursor;
+    Property LineNo : Integer Read FLineNo;
+  public
+    // Create a scanner for text, aLineNo is the line number in the markdown document.
+    constructor Create(aText : String; aLineNo : integer);
+    destructor Destroy; override;
+    // Bookmark current position of cursor
+    procedure BookMark;
+    // Return to bookmarked  position of cursor
+    procedure GotoBookMark;
+    // Check if there is a run (same characters) starting at current pos, and return the run.
+    // If checkbefore is true, checks whether the previous character is NOT part of the run and returns empty if it is.
+    function PeekRun(checkBefore : boolean): String;
+    // Peek at the aLength next characters and return them.
+    // Do not modify cursor position
+    function PeekLen(aLength : integer) : String;
+    // Peek at the next characters till one of the characters in aMatch is encountered.
+    // return them. Do not modify cursor position
+    function PeekUntil(aMatch : TSysCharSet) : String;
+    // Peek at the next characters till one of the characters in aMatch is encountered.
+    // return them. Do not modify cursor position
+    function PeekWhile(aMatch : TSysCharSet) : String;
+    // Look at the next character in the text. Advance cursor position.
+    function NextChar : char;
+    // Look at the next run of characters in the text. Advance cursor position.
+    function NextEquals : String; overload;
+    // Get the next aCount characters. Advance the cursor position.
+    function NextChars(aCount : integer) : String;
+    // Check that the text has S starting at the next character position
+    function Has(S : string) : boolean;
+    // Check if there is a second occurence after an occurence of string S at current position.
+    function FindMatchingOccurrence(const S : String) : boolean;
+    // Check if there is a second occurence after an occurence of string S at current position, second occurrence may not be preceded by ExcludeBefore.
+    function FindMatchingOccurrence(const S : String; ExcludeBefore : char) : boolean;
+    // Skip all whitespace, advance cursor position
+    procedure SkipWhitespace;
+    // Current location. Takes into account offset from aLineNo
+    function Location : TPosition;
+    // Are we at the end of the text ?
+    property EOF : boolean read GetEOF;
+    // Character at the current cursor position. Do not modify cursor position
+    property Peek : Char read GetPeek;
+    // Character at the next character position. Do not modify cursor position
+    property PeekNext : Char read GetPeekNext;
+    // Character at the next character position. Do not modify cursor position
+    property PeekPrevious : Char read GetPeekLast;
+    // Character after the next run of characters. Do not modify cursor position
+    property PeekEndRun : Char read GetPeekEndRun;
+  end;
+
+implementation
+
+uses MarkDown.Utils;
+
+{ TMarkDownTextScanner }
+
+constructor TMarkDownTextScanner.Create(aText: String; aLineNo : integer);
+begin
+  inherited Create;
+  FText:=aText;
+  FCursor:=1;
+  FLineNo:=aLineNo;
+  FPos.Line:=0;
+  FPos.Col:=1;
+end;
+
+destructor TMarkDownTextScanner.Destroy;
+begin
+  inherited;
+end;
+
+procedure TMarkDownTextScanner.GotoBookMark;
+begin
+  FCursor:=FMark;
+  FPos:=FMarkPos;
+end;
+
+function TMarkDownTextScanner.FindMatchingOccurrence(const S: String): boolean;
+var
+  i, len, LenText,lMax : integer;
+begin
+  Result:=false;
+  LenText:=Length(FText);
+  len:=Length(S);
+  lMax:=LenText-Len+1;
+  i:=FCursor+len+1;
+  while (not Result) and (i<=lMax) do
+    begin
+    Result:=(FText[i-1]<>s[1])
+            and ((i=lMax) or (FText[i+len] <> s[1]))
+            and (copy(FText,i,len) = s);
+    Inc(i);
+    end;
+end;
+
+function TMarkDownTextScanner.FindMatchingOccurrence(const S: String; ExcludeBefore: char): boolean;
+var
+  i, len, lenText, lMax : integer;
+begin
+  Result:=false;
+  len:=Length(s);
+  i:=FCursor+len;
+  LenText:=Length(FText);
+  lMax:=LenText-Len+1;
+  while not Result and (I<=lMax) do
+    begin
+    if FText[i]=ExcludeBefore then
+      exit(false);
+    Result:=(FText[i-1]<>s[1])
+            and ((i=lMax) or (FText[i+len]<>s[1]))
+            and (Copy(FText,i,len) = s);
+    Inc(i);
+    end;
+end;
+
+procedure TMarkDownTextScanner.SkipWhitespace;
+begin
+  while isWhitespaceChar(peek) do
+    NextChar();
+end;
+
+function TMarkDownTextScanner.GetEOF: boolean;
+begin
+  Result:=FCursor>Length(FText);
+end;
+
+function TMarkDownTextScanner.GetPeek: char;
+begin
+  if EOF then
+    Result:=#0
+  else
+    Result:=FText[FCursor];
+end;
+
+function TMarkDownTextScanner.GetPeekEndRun: char;
+var
+  i,Len : integer;
+  c : char;
+begin
+  Len:=Length(FText);
+  if (FCursor>=Len) then
+    Result:=#0
+  else
+    begin
+    i:=FCursor;
+    c:=FText[i];
+    while (i<=Len) and (FText[i]=c) do
+      Inc(i);
+    if (i>Len) then
+      Result:=#0
+    else
+      Result:=FText[i];
+    end;
+end;
+
+function TMarkDownTextScanner.GetPeekLast: char;
+begin
+  if FCursor = 1 then
+    Result:=#0
+  else
+    Result:=FText[FCursor-1];
+end;
+
+function TMarkDownTextScanner.GetPeekNext: char;
+begin
+  if FCursor >= Length(FText) then
+    Result:=#0
+  else
+    Result:=FText[FCursor+1];
+end;
+
+function TMarkDownTextScanner.PeekRun(checkBefore: boolean): String;
+var
+  i,Len : integer;
+  c : char;
+begin
+  c:=peek;
+  i:=FCursor;
+  if CheckBefore and (i>2) and (FText[i-1] = c) then
+    exit('');
+  Len:=Length(FText);
+  while (i<=Len) and (FText[i] = c) do
+    Inc(i);
+  Result:=Copy(FText,FCursor,I-FCursor);
+end;
+
+function TMarkDownTextScanner.NextChars(aCount: integer): String;
+
+var
+  i,lCount : integer;
+
+begin
+  Result:='';
+  SetLength(Result,aCount);
+  lCount:=0;
+  for i:=1 to aCount do
+    begin
+    if not EOF then
+      begin
+      Result[I]:=NextChar();
+      Inc(lCount);
+      end;
+    end;
+  if lCount<aCount then
+    SetLength(Result,lCount);
+end;
+
+function TMarkDownTextScanner.NextEquals: String;
+
+const
+  Delta = 10;
+
+var
+  c : char;
+  lLen,lCount : Integer;
+begin
+  Result:='';
+  lLen:=0;
+  lCount:=0;
+  c:=peek;
+  while peek=c do
+    begin
+    if lCount=lLen then
+      begin
+      Inc(lLen,Delta);
+      SetLength(Result,LLen);
+      end;
+    Inc(lCount);
+    Result[lCount]:=nextChar;
+    end;
+  SetLength(Result,lCount);
+end;
+
+function TMarkDownTextScanner.NextChar: char;
+
+begin
+  if EOF then
+    Result:=#0
+  else
+    begin
+    Result:=FText[FCursor];
+    Inc(FCursor);
+    if Result=#10 then
+    begin
+      FPos.Col:=1;
+      Inc(FPos.Line);
+    end
+    else
+      Inc(FPos.Col);
+    end;
+end;
+
+function TMarkDownTextScanner.Has(S: string): boolean;
+begin
+  Result:=peekLen(Length(s))=s;
+end;
+
+function TMarkDownTextScanner.Location: TPosition;
+begin
+  Result:=fPos;
+  Inc(Result.line,FLineNo);
+end;
+
+procedure TMarkDownTextScanner.BookMark;
+begin
+  FMark:=FCursor;
+  FMarkPos:=FPos;
+end;
+
+function TMarkDownTextScanner.PeekUntil(aMatch : TSysCharSet) : String;
+var
+  i,Len : integer;
+begin
+  i:=FCursor;
+  Len:=Length(FText);
+  while (i<=Len) and not CharInSet(FText[i],aMatch) do
+    Inc(i);
+  if (i>Len) then
+    Result:=''
+  else
+    Result:=Copy(FText,FCursor,I-FCursor);
+end;
+
+function TMarkDownTextScanner.PeekWhile(aMatch: TSysCharSet): String;
+var
+  i,Len : integer;
+begin
+  i:=FCursor;
+  Len:=Length(FText);
+  while (i<=Len) and CharInSet(FText[i],aMatch) do
+    Inc(i);
+  Result:=Copy(FText,FCursor,I-FCursor);
+end;
+
+function TMarkDownTextScanner.PeekLen(aLength: integer): String;
+
+begin
+  Result:=Copy(FText,FCursor,aLength);
+end;
+
+end.
+

+ 724 - 0
packages/fcl-md/src/markdown.utils.pas

@@ -0,0 +1,724 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Various text processing routines & helper classes for parsing Markdown.
+
+    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.Utils;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+{$IFDEF FPC_DOTTEDUNITS}
+  System.Classes, System.SysUtils, System.Contnrs, System.RegExpr;
+{$ELSE}
+  Classes, SysUtils, Contnrs, RegExpr;
+{$ENDIF}
+
+const
+  cSchemeStartChars =  ['a'..'z', 'A'..'Z'];
+  cSchemeChars = cSchemeStartChars + ['0'..'9', '+', '.', '-'];
+
+Type
+  TUnicodeCharDynArray = array of unicodechar;
+
+  { THashTable }
+
+  THashTable = class(TFPStringHashTable)
+    function Contains(const aKey : string) : boolean;
+    function TryGet(const aKey : string; out aValue : string) : boolean;
+  end;
+
+  { TGFPObjectList }
+  // Included here to be able to compile with 3.2.X
+  generic TGFPObjectList<T : TObject> = class (TFPObjectList)
+  private
+    Type
+       { TObjectEnum }
+       TObjectEnum = Class
+         FList : TFPObjectList;
+         FIdx : Integer;
+         constructor create(aList : TFPObjectList);
+         function GetCurrent : T;
+         function MoveNext: Boolean;
+         property Current : T read GetCurrent;
+       end;
+    function GetElement(aIndex: Integer): T;
+    procedure SetElement(aIndex: Integer; AValue: T);
+  Public
+    function getenumerator : TObjectEnum;
+    function add(aElement : T) : integer;
+    property Elements[aIndex: Integer] : T read GetElement Write SetElement; default;
+  end;
+
+{ 
+  string operations
+  Tab characters are counted as tabstops every 4 characters, 
+  Note that this is *not* the same as saying that "a tab equals 4 space characters".
+  so #32#9 and #32#32#9 are equivalent to 4 spaces, not 5 or 6 respectively...
+}  
+
+// Is aChar a whitespace character ? 
+function IsWhitespaceChar(aChar: char): boolean;
+// Does S consist of only whitespace characters ? 
+function IsWhitespace(const S: String): boolean;
+// must character aChar be escaped ?
+function MustEscape(C : char) : boolean;
+// Are all characters in S identical ?
+function IsStringOfChar(const S : String) : boolean;
+// Copy from the start of S all characters up to, but not including, the first character in aExclude
+function CopyUpTo(const S : String; aExclude : TSysCharSet) : String;
+// Copy from the start of S all characters after characters in aSkip
+function CopySkipped(const S : String; aSkip : TSysCharSet) : String;
+// Copy from the start of S all characters that match
+function CopyMatching(const S : String; aMatches : TSysCharSet) : String;
+{ 
+  allows up to aWSLen whitespace characters before aMatch;
+  Returns true if a match was found. 
+  Additionally returns the number of whitespace characters to remove from the start to get to aMatch.
+}
+
+function StartsWithWhitespace(const S : String; aMatch : AnsiChar; out aLength : integer; aWSLen : integer = 3) : boolean; overload;
+function StartsWithWhitespace(const S : String; aMatch : TSysCharSet; out aLength : integer; aWSLen : integer = 3) : boolean; overload;
+function StartsWithWhitespace(const S : String; aMatch : String; out aLength : integer; aWSLen : integer = 3) : boolean; overload;
+// Returns the number of space characters. Tab is a tabulator of 4
+function LeadingWhitespace(const S : String) : integer; inline;
+// Returns the number of space characters. Tab is a tabulator of 4.
+function LeadingWhitespace(const S : String; out aTabs : integer) : integer; inline;
+// Returns the number of space characters. Tab is a tabulator of 4. Returns the number of tab characters and characters considered whitespace.
+function LeadingWhitespace(const S : String; out aTabs, aWhitespaceChars : integer) : integer;
+// Returns the number of space characters. Tab is a tabulator of 4
+function LengthWhitespaceCorrected(const S : String) : integer;
+// Remove up to count spaces. Tabs are taken into account.
+function RemoveLeadingWhiteSpace(const S : String; aCount : integer) : String;
+// Remove ALL whitespace from S
+function StripWhitespace(const S : String) : String;
+// HTML escaping of < > & and " for a single character aChar
+function HtmlEscape(aChar : char) : String; overload;
+// HTML escaping of < > & and " for all characters in S
+function HtmlEscape(const S : String) : String; overload;
+// URL escape of aChar for display in HTML (so & is escaped)
+function UrlEscape(aChar : UnicodeChar) : String; overload;
+// URL escape of all characters in S
+function UrlEscape(const S : String) : String; overload;
+// Is the string S an absolute URI ?
+function isAbsoluteUri(const S : String) : boolean;
+// Is S a valid email address
+function IsValidEmail(const S : String) : boolean;
+// Parse entity string
+function ParseEntityString(aEntities : TFPStringHashTable; const aEntity : String): String;
+// Check if S ends on an entity start character, and if so, return the length of the entity
+function CheckForTrailingEntity(Const S : String) : integer;
+// Return true if aContent is a match for regular expression aRegex
+function IsRegexMatch(const aContent, aRegex: String): boolean;
+// Is aChar a Unicode punctuation character ?
+function IsUnicodePunctuation(aChar : UnicodeChar) : boolean;
+// Count the number of characters aChar at the start of aLine
+function CountStartChars(const aLine : string; aChar : Char) : integer;
+// Convert the string S to an array of unicode characters
+function ToUnicodeChars(const S : String) : TUnicodeCharDynArray;
+// Transform tabulators to spaces in leading whitespace, taking into account the above definition of tabulator.
+Function TransformTabs(const aLine : string) : string;
+
+implementation
+
+uses 
+{$IFDEF FPC_DOTTEDUNITS}
+  System.StrUtils, System.CodePages.unicodedata;
+{$ELSE}
+  StrUtils, UnicodeData;
+{$ENDIF}
+
+function CountStartChars(const aLine : string; aChar : Char) : integer;
+var
+  I : integer;
+begin
+  Result:=0;
+  For I:=1 to length(aLine) do
+    begin
+    if aLine[I]<>aChar then
+      exit;
+    inc(Result);
+    end;
+end;
+
+function ToUnicodeChars(const S: String): TUnicodeCharDynArray;
+
+var
+  i, lLen: integer;
+  U: UnicodeString;
+
+begin
+  Result:=[];
+  U:=UTF8Decode(s);
+  lLen:=Length(U);
+  SetLength(Result,lLen);
+  for i:=1 to lLen do
+    Result[i-1]:=U[i];
+end;
+
+
+function IsUnicodePunctuation(aChar: UnicodeChar): boolean;
+
+var
+  Cat : Byte;
+
+begin
+  // fast check
+  Result:=Not (aChar in ['0'..'9','a'..'z','A'..'Z','_']);
+  if not Result then
+    Exit;
+  Result:=(Ord(aChar)<128) or (Ord(aChar)>=LOW_SURROGATE_BEGIN);
+  if Result then 
+    exit;
+  Cat:=GetProps(Ord(aChar))^.Category;
+  Result:=(UGC_OtherNumber<Cat);
+end;
+
+
+function HtmlEscape(aChar: char): String;
+
+begin
+  case aChar of
+    '<' : Result:='&lt;';
+    '>' : Result:='&gt;';
+    '"' : Result:='&quot;';
+    '&' : Result:='&amp;';
+  else
+    Result:=aChar;
+  end;
+end;
+
+
+function HtmlEscape(const S: String): String;
+
+var
+  C : char;
+  
+begin
+  Result:='';
+  for C in S do
+    Result:=Result+HtmlEscape(C);
+end;
+
+
+function CopySkipped(const S: String; aSkip: TSysCharSet): String;
+
+var
+  lLen,Idx : integer;
+  
+begin
+  Result:=S;
+  lLen:=Length(S);
+  Idx:=0;
+  while (Idx<lLen) and (CharInSet(S[Idx+1],aSkip)) do
+    Inc(Idx);
+  Delete(Result,1,Idx);
+end;
+
+
+function IsStringOfChar(const S: String): boolean;
+
+var
+  lLen,I : integer;
+  C : char;
+
+begin
+  Result:=true;
+  lLen:=Length(S);
+  if lLen=0 then
+    Exit;
+  C:=S[1];
+  i:=2;
+  While Result and (I<=lLen) do
+    begin
+    Result:=(C=S[i]);
+    inc(i);
+    end;
+end;
+
+function CopyUpTo(const S: String; aExclude: TSysCharSet): String;
+
+var
+  lLen, Idx : Integer;
+
+begin
+  Result:=S;
+  lLen:=Length(S);
+  Idx:=1;
+  while (Idx<=lLen) and not CharInSet(S[Idx],aExclude) do
+    inc(Idx);
+  SetLength(Result,Idx-1);
+end;
+
+
+function CopyMatching(const S: String; aMatches: TSysCharSet): String;
+
+var
+  lLen, Idx : integer;
+  
+begin
+  Idx:=1;
+  lLen:=Length(S);
+  while (Idx<=lLen) and CharInSet(S[Idx],aMatches) do
+    inc(Idx);
+  if (Idx>Length(S)) then
+    Result:=S
+  else
+    Result:=copy(S,1,Idx-1);
+end;
+
+
+function StartsWithWhitespace(const S: String; aMatch: AnsiChar; out aLength: integer; aWSLen: integer): boolean;
+
+var
+  lLen, Idx : integer;
+
+begin
+  if S='' then
+    exit(false);
+  aLength:=1;
+  lLen:=Length(S);
+  // todo: change to use leadingwhitespace to handle tabs.
+  for Idx:=1 to aWSLen do
+    begin
+    if (aLength<=lLen) and (S[aLength]=' ') then
+      inc(aLength);
+    end;
+  Result:=S[aLength]=aMatch;
+  if Result then 
+    Dec(aLength);
+end;
+
+function StartsWithWhitespace(const S: String; aMatch: TSysCharSet; out aLength: integer; aWSLen: integer): boolean;
+var
+  lLen, Idx : integer;
+
+begin
+  if S='' then
+    exit(false);
+  aLength:=1;
+  lLen:=Length(S);
+  // todo: change to use leadingwhitespace to handle tabs.
+  for Idx:=1 to aWSLen do
+    begin
+    if (aLength<=lLen) and (S[aLength]=' ') then
+      inc(aLength);
+    end;
+  Result:=S[aLength] in aMatch;
+  if Result then
+    Dec(aLength);
+end;
+
+function StartsWithWhitespace(const S: String; aMatch: String; out aLength: integer; aWSLen: integer): boolean;
+
+var
+  Len, I : integer;
+
+begin
+  if S='' then
+    exit(false);
+  aLength:=1;
+  Len:=Length(S);
+  // todo: change to use leadingwhitespace to handle tabs.
+  for i:=1 to aWSLen do
+    begin
+    if (aLength<=Len) and (S[aLength]=' ') then
+      inc(aLength);
+    end;
+  Result:=Copy(s,aLength,Length(aMatch))=aMatch;
+  if Result then 
+    Dec(aLength);
+end;
+
+
+function LengthWhitespaceCorrected(const S: String): integer;
+
+var
+  lDelta,i : integer;
+
+begin
+  Result:=0;
+  for i:=1 to length(S) do
+    begin
+    if s[i]=#9 then
+      lDelta:=4-((i-1) mod 4)
+    else
+      lDelta:=1;
+    inc(Result,lDelta);
+    end;
+end;
+
+function LeadingWhitespace(const S: String; out aTabs, aWhitespaceChars: integer): integer;
+
+var
+  i,lLen,lDelta : integer;
+
+begin
+  aTabs:=0;
+  Result:=0;
+  i:=0;
+  lLen:=Length(S);
+  while (i<lLen) and CharInSet(S[i+1], [' ',#9]) do
+    begin
+    if S[i+1]=#9 then
+      begin
+      inc(aTabs);
+      lDelta:=4-(Result mod 4);
+      end
+    else
+      lDelta:=1;
+    inc(Result,lDelta);
+    inc(i);
+    end;
+  aWhitespaceChars:=i;
+end;
+
+function LeadingWhitespace(const S: String): integer;
+
+var
+  lTabs,lChars : integer;
+
+begin
+  Result:=LeadingWhitespace(S,lTabs,lChars);
+end;
+
+function LeadingWhitespace(const S: String; out aTabs: integer): integer;
+
+var
+  lChars : integer;
+
+begin
+  Result:=LeadingWhitespace(S,aTabs,lChars);
+end;
+
+
+function RemoveLeadingWhiteSpace(const S: String; aCount: integer): String;
+
+var
+  Len, Idx, Delta, lCount : integer;
+
+begin
+  Result:='';
+  Idx:=0; // Index of first non-whitespace char
+  lCount:=0; // whitespace count, taking into account tabstop
+  Len:=Length(S);
+  while (Idx<=Len) and (lCount<aCount) do
+    begin
+    inc(Idx);
+    if (Idx<=Len) and (S[Idx] in [' ',#9]) then
+      begin
+      if (S[Idx]=' ') then
+        Delta:=1
+      else if s[Idx]=#9 then
+        Delta:=4 - (lCount mod 4);
+      inc(lCount,Delta);  
+      end
+    else
+      break;
+    end;
+  // create remainder spaces
+  if lCount>aCount then
+    Result:=StringOfChar(' ',lCount-aCount);
+  // add non-whitespace
+  Result:=Result+Copy(S,Idx+1,Len-Idx);
+end;
+
+function StripWhitespace(const S: String): String;
+
+var
+  lCount : integer;
+  C : Char;
+
+begin
+  Result:='';
+  SetLength(Result,Length(S));
+  lCount:=0;
+  for C in S do
+    if not isWhitespace(C) then
+      begin
+      inc(lCount);
+      Result[lCount]:=c;
+      end;
+  SetLength(Result,lCount);
+end;
+
+function UrlEscape(aChar: UnicodeChar): String;
+
+var
+  b : TBytes;
+  i : integer;
+
+begin
+  case aChar of
+    '&' :
+      // not escaped in URL but is escaped in html
+      Result:='&amp;';
+    '\', '[', ']', '"', '`' :
+      Result:='%'+IntToHex(ord(aChar),2);
+  else
+    if ord(aChar) > $7F then
+      begin
+      b:=TEncoding.UTF8.GetBytes(aChar);
+      Result:='';
+      for i:=0 to length(b) - 1 do
+        Result:=Result + '%'+IntToHex(b[i],2);
+      end
+    else if ord(aChar) > 126 then
+      Result:='%'+IntToHex(ord(aChar),2)
+    else
+      Result:=AnsiChar(aChar);
+  end;
+end;
+
+function UrlEscape(const S: String): String;
+
+var
+  C : UnicodeChar;
+
+begin
+  Result:='';
+  for C in ToUnicodeChars(S) do
+    Result:=Result+urlEscape(C);
+end;
+
+function IsWhitespaceChar(aChar: char): boolean;
+
+begin
+  Result:=CharInSet(aChar,[#9,#10,#32]);
+end;
+
+function IsWhitespace(const S: String): boolean;
+
+var
+  C : Char;
+
+begin
+  Result:=true;
+  for C in s do
+    if not isWhitespaceChar(C) then
+      exit(false);
+end;
+
+function MustEscape(C : char) : boolean;
+
+begin
+  Result:=Pos(C,'!"#$%&''()*+,-./:;<=>?@[\]^_`{|}~')>0;
+end;
+
+function ParseEntityString(aEntities : TFPStringHashTable; const aEntity : String): String;
+
+var
+  S : String;
+  C : UnicodeChar;
+  i, Len : integer;
+  
+begin
+  S:=copy(aEntity,2,length(aEntity)-2); // Strip & and ;
+  Result:=aEntities.Items[S];
+  if Result<>'' then
+    Exit;
+  Len:=Length(S);  
+  if (Len>0) and (Len<=9) and (S[1]='#') and TryStrToInt(Copy(S,2,Len-1),i)  then
+    begin
+    if (I<=0) or (i>65535) then
+      C:=#$FFFD
+    else
+      C:=UnicodeChar(i);
+    Result:=UTF8Encode(C);
+    end;
+end;
+
+function CheckForTrailingEntity(const S: String): integer;
+
+var
+  Tmp : String;
+  C : char;
+  p,Len : Integer;
+  
+begin
+  Result:=0;
+  P:=RPos('&',S);
+  if P=0 then
+    exit;
+  Len:=Length(S);  
+  Tmp:=Copy(S,P+1,Len-P-1);
+  for C in Tmp do
+    if not CharInSet(C, ['a'..'z', 'A'..'Z', '0'..'9']) then
+      exit;
+   exit(Length(tmp)+2);
+end;
+
+function IsRegexMatch(const aContent, aRegex: String): boolean;
+
+var
+  lRegex : TRegExpr;
+  
+begin
+  Result:=False;
+  if aContent = '' then
+    Exit;
+  lRegex:=TRegExpr.create(aRegex);
+  try
+    Result:=lRegex.exec(aContent);
+  finally
+    lRegex.Free;
+  end;
+end;
+
+function TransformTabs(const aLine: string): string;
+
+var
+  len,tabs,wsCount : integer;
+
+begin
+  Len:=LeadingWhitespace(aLine,tabs,wsCount);
+  if (Len=0) or (Tabs=0) then
+    Result:=aLine
+  else
+    Result:=StringOfChar(' ',Len)+Copy(aLine,wsCount+1,Length(aLine)-wsCount);
+end;
+
+
+{ THashTable }
+
+function THashTable.Contains(const aKey: string): boolean;
+
+begin
+  Result:=Find(aKey)<>Nil;
+end;
+
+function THashTable.TryGet(const aKey: string; out aValue: string): boolean;
+
+var
+  N : THTStringNode;
+begin
+  N:=THTStringNode(Find(aKey));
+  Result:=Assigned(N);
+  if Result then
+    aValue:=N.Data;
+end;
+
+
+{ TGFPObjectList }
+
+function TGFPObjectList.GetElement(aIndex: Integer): T;
+
+begin
+  Result:=T(Items[aIndex]);
+end;
+
+procedure TGFPObjectList.SetElement(aIndex: Integer; AValue: T);
+
+begin
+  Items[aIndex]:=aValue;
+end;
+
+function TGFPObjectList.getenumerator: TObjectEnum;
+
+begin
+  Result:=TObjectEnum.Create(Self);
+end;
+
+function TGFPObjectList.add(aElement: T): integer;
+begin
+  Result:=Inherited add(aElement);
+end;
+
+{ TGFPObjectList.TObjectEnum }
+
+constructor TGFPObjectList.TObjectEnum.create(aList: TFPObjectList);
+begin
+  FList:=aList;
+  FIdx:=-1;
+end;
+
+function TGFPObjectList.TObjectEnum.GetCurrent: T;
+begin
+  If FIdx<0 then
+    Result:=Nil
+  else
+    Result:=T(FList[FIdx]);
+end;
+
+function TGFPObjectList.TObjectEnum.MoveNext: Boolean;
+begin
+  Inc(FIdx);
+  Result:=FIdx<FList.Count;
+end;
+
+
+function isAbsoluteUri(const S : String) : boolean;
+
+var
+  lScheme, lTail : String;
+  i,p,lLen : integer;
+
+begin
+  Result:=False;
+  p:=Pos(':',S);
+  if p=0 then
+    Exit;
+  lScheme:=Copy(S,1,P-1);
+  lLen:=Length(lScheme);
+  if (lLen<2) or (lLen>32) then
+    Exit;
+  if not CharInSet(lScheme[1], cSchemeStartChars) then
+    Exit;
+  for i:=2 to lLen do
+    if not CharInSet(lScheme[i], cSchemeChars) then
+      Exit;
+  lTail:=S;
+  Delete(lTail,1,P);
+  lLen:=Length(lTail);
+  for i:=1 to lLen do
+    if CharInSet(lTail[i], [' ', #9, #10, '<']) then
+      Exit;
+  Result:=true;
+end;
+
+function IsValidEmail(const S : String) : boolean;
+
+type
+  TState = (sNeutral,sUser,sHost,sDomain);
+
+var
+  lState : TState;
+  c : char;
+
+begin
+  Result:=False;
+  if s.CountChar('@') <> 1 then
+    Exit;
+  lState:=sNeutral;
+  for c in s do
+    Case lState of
+    sNeutral:
+      if c='@' then
+        Exit
+      else
+        lState:=sUser;
+    sUser:
+      if c='@' then
+        lState:=sHost;
+    else
+      if c='.' then
+        lState:=sDomain
+      else if c='+' then
+        Exit;
+    end;
+  Result:=lState=sDomain;
+end;
+
+end.
+

+ 1 - 0
packages/fcl-md/tests/README.md

@@ -0,0 +1 @@
+This directory contains the unit tests for the markdown parser.

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

@@ -0,0 +1,156 @@
+<?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="testmd"/>
+      <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>
+    <RequiredPackages>
+      <Item>
+        <PackageName Value="FCL"/>
+      </Item>
+    </RequiredPackages>
+    <Units>
+      <Unit>
+        <Filename Value="testmd.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="utest.markdown.utils.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="../src/markdown.elements.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="MarkDown.Elements"/>
+      </Unit>
+      <Unit>
+        <Filename Value="../src/markdown.htmlentities.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="Markdown.HTMLEntities"/>
+      </Unit>
+      <Unit>
+        <Filename Value="../src/markdown.htmlrender.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="Markdown.HtmlRender"/>
+      </Unit>
+      <Unit>
+        <Filename Value="../src/markdown.inlinetext.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="MarkDown.InlineText"/>
+      </Unit>
+      <Unit>
+        <Filename Value="../src/markdown.line.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="MarkDown.Line"/>
+      </Unit>
+      <Unit>
+        <Filename Value="../src/markdown.parser.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="MarkDown.Parser"/>
+      </Unit>
+      <Unit>
+        <Filename Value="../src/markdown.render.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="MarkDown.Render"/>
+      </Unit>
+      <Unit>
+        <Filename Value="../src/markdown.scanner.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="MarkDown.Scanner"/>
+      </Unit>
+      <Unit>
+        <Filename Value="../src/markdown.utils.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="MarkDown.Utils"/>
+      </Unit>
+      <Unit>
+        <Filename Value="utest.markdown.scanner.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="UTest.Markdown.Scanner"/>
+      </Unit>
+      <Unit>
+        <Filename Value="utest.markdown.inlinetext.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="UTest.Markdown.InlineText"/>
+      </Unit>
+      <Unit>
+        <Filename Value="utest.markdown.htmlrender.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="UTest.Markdown.HTMLRender"/>
+      </Unit>
+      <Unit>
+        <Filename Value="utest.markdown.parser.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="../src/markdown.processors.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="Markdown.Processors"/>
+      </Unit>
+      <Unit>
+        <Filename Value="utest.markdown.fpdocrender.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="UTest.Markdown.FPDocRender"/>
+      </Unit>
+      <Unit>
+        <Filename Value="../src/markdown.fpdocrender.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="Markdown.FPDocRender"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="testmd"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <OtherUnitFiles Value="../src"/>
+      <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+    <Linking>
+      <Debugging>
+        <DebugInfoType Value="dsDwarf3"/>
+        <UseHeaptrc Value="True"/>
+      </Debugging>
+    </Linking>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions>
+      <Item>
+        <Name Value="EAbort"/>
+      </Item>
+      <Item>
+        <Name Value="ECodetoolError"/>
+      </Item>
+      <Item>
+        <Name Value="EFOpenError"/>
+      </Item>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 31 - 0
packages/fcl-md/tests/testmd.lpr

@@ -0,0 +1,31 @@
+program testmd;
+
+{$mode objfpc}{$H+}
+
+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;
+
+type
+
+  { TMyTestRunner }
+
+  TMyTestRunner = class(TTestRunner)
+  protected
+  // override the protected methods of TTestRunner to customize its behavior
+  end;
+
+var
+  Application: TMyTestRunner;
+
+begin
+  DefaultRunAllTests:=True;
+  DefaultFormat:=fPlain;
+  Application := TMyTestRunner.Create(nil);
+  Application.Initialize;
+  Application.Title := 'FPCUnit Console test runner';
+  Application.Run;
+  Application.Free;
+end.

+ 390 - 0
packages/fcl-md/tests/utest.markdown.fpdocrender.pas

@@ -0,0 +1,390 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown FPDoc 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.FPDocRender;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, fpcunit, testregistry,
+  MarkDown.Elements,
+  MarkDown.FPDocRender;
+
+type
+
+  { TTestFPDocRender }
+
+  TTestFPDocRender = Class(TTestCase)
+  private
+    FFPDocRenderer : TMarkDownFPDocRenderer;
+    FDocument: TMarkDownDocument;
+    FParent : TMarkDownBlock;
+  Public
+    const
+      cIndent    = '        ';
+      cNLIndent  = sLineBreak+cIndent;
+      cNLIndent2  = sLineBreak+cIndent+'  ';
+      cNLIndent4  = sLineBreak+cIndent+'    ';
+
+    Procedure SetUp; override;
+    Procedure TearDown; override;
+    Procedure StartDoc;
+    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 aFPDoc : string; aEnvelope : boolean = False);
+    Property Renderer : TMarkDownFPDocRenderer Read FFPDocRenderer;
+    Property Document : TMarkDownDocument Read FDocument;
+  Published
+    procedure TestHookup;
+    procedure TestEmpty;
+    procedure TestEmptyPackageName;
+    procedure TestTextBlockEmpty;
+    procedure TestTextBlockText;
+    procedure TestTextBlockTextStrong;
+    procedure TestTextBlockTextEmph;
+    procedure TestTextBlockTextDelete;
+    procedure TestTextBlockTextStrongEmph;
+    procedure TestTextBlockTextStrongEmphSplit1;
+    procedure TestTextBlockTextStrongEmphSplit2;
+    procedure TestPragraphBlockEmpty;
+    procedure TestPragraphBlockText;
+    procedure TestQuotedBlockEmpty;
+    procedure TestQuotedBlockText;
+    procedure TestUnorderedListEmpty;
+    procedure TestUnorderedListOneItem;
+  end;
+
+implementation
+
+{ TTestFPDocRender }
+
+procedure TTestFPDocRender.SetUp;
+
+begin
+  FFPDocRenderer:=TMarkDownFPDocRenderer.Create(Nil);
+  FDocument:=TMarkDownDocument.Create(Nil,1);
+  FParent:=FDocument;
+end;
+
+
+procedure TTestFPDocRender.TearDown;
+
+begin
+  FParent:=nil;
+  FreeAndNil(FDocument);
+  FreeAndNil(FFPDocRenderer);
+end;
+
+procedure TTestFPDocRender.StartDoc;
+var
+  l : TMarkDownBlock;
+begin
+  CreateHeadingBlock('unit1',1);
+  CreateHeadingBlock('a',2);
+  CreateHeadingBlock('descr',3);
+end;
+
+
+function TTestFPDocRender.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 TTestFPDocRender.CreateParagraphBlock(const aTextNode: string): TMarkdownBlock;
+
+begin
+  Result:=TMarkDownParagraphBlock.Create(FParent,1);
+  if aTextNode<>'' then
+    CreateTextBlock(Result,aTextNode,aTextNode);
+end;
+
+function TTestFPDocRender.CreateQuotedBlock(const aTextNode: string): TMarkdownBlock;
+
+begin
+  Result:=TMarkDownQuoteBlock.Create(FParent,1);
+  if aTextNode<>'' then
+    CreateTextBlock(Result,aTextNode,aTextNode);
+end;
+
+function TTestFPDocRender.CreateHeadingBlock(const aTextNode: string; aLevel: integer): TMarkdownBlock;
+
+begin
+  Result:=TMarkDownHeadingBlock.Create(FParent,1,aLevel);
+  if aTextNode<>'' then
+    CreateTextBlock(Result,aTextNode,aTextNode);
+end;
+
+function TTestFPDocRender.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 TTestFPDocRender.CreateListBlock(aOrdered: boolean; const aListItemText: string): TMarkDownListBlock;
+
+begin
+  Result:=TMarkDownListBlock.Create(FParent,1);
+  Result.ordered:=aOrdered;
+  if aListItemText<>'' then
+    CreateListItemBlock(Result,aListItemText);
+end;
+
+function TTestFPDocRender.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 TTestFPDocRender.TestRender(const aFPDoc: string; aEnvelope: boolean);
+
+const
+  prefix = '<?xml version="1.0" encoding="utf-8"?>'+sLineBreak
+             +'<fpdoc-descriptions>'+sLineBreak
+             +'  <package>'+sLineBreak
+             +'    <module name="unit1">'+sLineBreak
+             +'      <element name="a">'+sLineBreak;
+
+  prefixEmpty = prefix
+             + '        <descr/>'+sLineBreak;
+
+  prefixContent = prefix
+             + '        <descr>';
+  Suffix    = '      </element>'+sLineBreak
+            + '    </module>'+sLineBreak
+            + '  </package>'+sLineBreak
+            + '</fpdoc-descriptions>';
+
+  SuffixContent = '</descr>'+sLineBreak
+            + Suffix;
+  SuffixEmpty = Suffix;
+
+var
+  L : TStrings;
+  lFPDoc: string;
+
+begin
+  L:=TstringList.Create;
+  try
+    L.SkipLastLineBreak:=True;
+    Renderer.RenderDocument(FDocument,L);
+    if not aEnvelope then
+      lFPDoc:=aFPDoc
+    else
+      begin
+      if aFPDoc='' then
+        lFPDoc:=PrefixEmpty+SuffixEmpty
+      else
+        lFPDoc:=PrefixContent+aFpdoc+SuffixContent
+      end;
+    assertEquals('Correct FPDoc: ',lFPDoc,L.Text);
+  finally
+    L.Free;
+  end;
+end;
+
+
+procedure TTestFPDocRender.TestHookup;
+
+begin
+  AssertNotNull('Have renderer',FFPDocRenderer);
+  AssertNotNull('Have document',FDocument);
+  AssertEquals('Have empty document',0,FDocument.blocks.Count);
+end;
+
+
+procedure TTestFPDocRender.TestEmpty;
+
+begin
+  TestRender('<?xml version="1.0" encoding="utf-8"?>'+sLineBreak
+             +'<fpdoc-descriptions>'+sLineBreak
+             +'  <package/>'+sLineBreak
+             +'</fpdoc-descriptions>');
+end;
+
+procedure TTestFPDocRender.TestEmptyPackageName;
+
+begin
+  Renderer.PackageName:='a';
+  TestRender('<?xml version="1.0" encoding="utf-8"?>'+sLineBreak
+             +'<fpdoc-descriptions>'+sLineBreak
+             +'  <package name="a"/>'+sLineBreak
+             +'</fpdoc-descriptions>');
+end;
+
+
+procedure TTestFPDocRender.TestTextBlockEmpty;
+
+begin
+  StartDoc;
+  CreateTextBlock(Document,'a','');
+  TestRender('',True);
+end;
+
+
+procedure TTestFPDocRender.TestTextBlockText;
+
+begin
+  StartDoc;
+  CreateTextBlock(Document,'a','a');
+  TestRender('a',True);
+end;
+
+
+procedure TTestFPDocRender.TestTextBlockTextStrong;
+
+begin
+  StartDoc;
+  CreateTextBlock(Document,'a','a',[nsStrong]);
+  TestRender(cNlIndent2+'<b>a</b>'+CnlIndent,True);
+end;
+
+
+procedure TTestFPDocRender.TestTextBlockTextEmph;
+
+begin
+  StartDoc;
+  CreateTextBlock(Document,'a','a',[nsEmph]);
+  TestRender(cNlIndent2+'<i>a</i>'+cNlIndent,True);
+end;
+
+
+procedure TTestFPDocRender.TestTextBlockTextDelete;
+
+begin
+  StartDoc;
+  CreateTextBlock(Document,'a','a',[nsDelete]);
+  TestRender(cNlIndent2+'<u>a</u>'+cNlindent,True);
+end;
+
+
+procedure TTestFPDocRender.TestTextBlockTextStrongEmph;
+
+begin
+  StartDoc;
+  CreateTextBlock(Document,'a','a',[nsStrong,nsEmph]);
+  TestRender(cNlIndent2+'<b>'+cNLIndent4+'<i>a</i>'+cNlIndent2+'</b>'+cNlIndent,True);
+end;
+
+
+procedure TTestFPDocRender.TestTextBlockTextStrongEmphSplit1;
+
+var
+  lBlock : TMarkDownTextBlock;
+
+begin
+  StartDoc;
+  lBlock:=CreateTextBlock(Document,'a','a ',[nsStrong]);
+  AppendTextNode(lBlock,'b',[nsStrong,nsemph]);
+  TestRender(cNlIndent2+'<b>a <i>b</i>'+cNlIndent2+'</b>'+cNlIndent,True);
+end;
+
+
+procedure TTestFPDocRender.TestTextBlockTextStrongEmphSplit2;
+
+var
+  lBlock : TMarkDownTextBlock;
+begin
+  StartDoc;
+  lBlock:=CreateTextBlock(Document,'a','a',[nsEmph,nsStrong]);
+  AppendTextNode(lBlock,' b',[nsStrong]);
+  TestRender(cNlIndent2+'<b>'+cNlIndent4+'<i>a</i> b</b>'+cNlIndent,True);
+end;
+
+
+procedure TTestFPDocRender.TestPragraphBlockEmpty;
+
+begin
+  StartDoc;
+  CreateParagraphBlock('');
+  TestRender(cNlIndent2+'<p/>'+cNlIndent,true);
+end;
+
+
+procedure TTestFPDocRender.TestPragraphBlockText;
+
+begin
+  StartDoc;
+  CreateParagraphBlock('a');
+  TestRender(cNlIndent2+'<p>a</p>'+cNlIndent,true);
+end;
+
+
+procedure TTestFPDocRender.TestQuotedBlockEmpty;
+
+begin
+  StartDoc;
+  CreateQuotedBlock('');
+  TestRender(cNlIndent2+'<remark/>'+cNlIndent,true);
+end;
+
+
+procedure TTestFPDocRender.TestQuotedBlockText;
+
+begin
+  StartDoc;
+  CreateQuotedBlock('a');
+  TestRender(cNlIndent2+'<remark>a</remark>'+cNlIndent,true);
+end;
+
+procedure TTestFPDocRender.TestUnorderedListEmpty;
+
+begin
+  StartDoc;
+  CreateListBlock(false,'');
+  TestRender(cNlIndent2+'<ul/>'+cNlIndent,True);
+end;
+
+
+procedure TTestFPDocRender.TestUnorderedListOneItem;
+
+begin
+  StartDoc;
+  CreateListBlock(false,'a');
+  TestRender(cNlIndent2+'<ul>'+cNlIndent4+'<li>a</li>'+cNlIndent2+'</ul>'+cNlIndent,True);
+end;
+
+
+initialization
+  Registertest(TTestFPDocRender);
+end.
+

+ 372 - 0
packages/fcl-md/tests/utest.markdown.htmlrender.pas

@@ -0,0 +1,372 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown HTML 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.HTMLRender;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, fpcunit, testregistry,
+  MarkDown.Elements,
+  MarkDown.HtmlRender;
+
+type
+
+  { TTestHTMLRender }
+
+  TTestHTMLRender = Class(TTestCase)
+  private
+    FHTMLRenderer : TMarkDownHTMLRenderer;
+    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 aHTML : string);
+    Property Renderer : TMarkDownHTMLRenderer Read FHTMLRenderer;
+    Property Document : TMarkDownDocument Read FDocument;
+  Published
+    procedure TestHookup;
+    procedure TestEmpty;
+    procedure TestEmptyNoEnvelope;
+    procedure TestEmptyTitle;
+    procedure TestEmptyHead;
+    procedure TestTextBlockEmpty;
+    procedure TestTextBlockText;
+    procedure TestTextBlockTextStrong;
+    procedure TestTextBlockTextEmph;
+    procedure TestTextBlockTextDelete;
+    procedure TestTextBlockTextStrongEmph;
+    procedure TestTextBlockTextStrongEmphSplit1;
+    procedure TestTextBlockTextStrongEmphSplit2;
+    procedure TestPragraphBlockEmpty;
+    procedure TestPragraphBlockText;
+    procedure TestQuotedBlockEmpty;
+    procedure TestQuotedBlockText;
+    procedure TestHeadingBlockEmpty;
+    procedure TestHeadingBlockText;
+    procedure TestHeadingBlockTextLevel2;
+    procedure TestUnorderedListEmpty;
+    procedure TestUnorderedListOneItem;
+  end;
+
+implementation
+
+{ TTestHTMLRender }
+
+procedure TTestHTMLRender.SetUp;
+
+begin
+  FHTMLRenderer:=TMarkDownHTMLRenderer.Create(Nil);
+  FDocument:=TMarkDownDocument.Create(Nil,1);
+end;
+
+
+procedure TTestHTMLRender.TearDown;
+
+begin
+  FreeAndNil(FDocument);
+  FreeAndNil(FHTMLRenderer);
+end;
+
+
+function TTestHTMLRender.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 TTestHTMLRender.CreateParagraphBlock(const aTextNode: string): TMarkdownBlock;
+
+begin
+  Result:=TMarkDownParagraphBlock.Create(FDocument,1);
+  if aTextNode<>'' then
+    CreateTextBlock(Result,aTextNode,aTextNode);
+end;
+
+function TTestHTMLRender.CreateQuotedBlock(const aTextNode: string): TMarkdownBlock;
+
+begin
+  Result:=TMarkDownQuoteBlock.Create(FDocument,1);
+  if aTextNode<>'' then
+    CreateTextBlock(Result,aTextNode,aTextNode);
+end;
+
+function TTestHTMLRender.CreateHeadingBlock(const aTextNode: string; aLevel: integer): TMarkdownBlock;
+
+begin
+  Result:=TMarkDownHeadingBlock.Create(FDocument,1,aLevel);
+  if aTextNode<>'' then
+    CreateTextBlock(Result,aTextNode,aTextNode);
+end;
+
+function TTestHTMLRender.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 TTestHTMLRender.CreateListBlock(aOrdered: boolean; const aListItemText: string): TMarkDownListBlock;
+
+begin
+  Result:=TMarkDownListBlock.Create(FDocument,1);
+  Result.ordered:=aOrdered;
+  if aListItemText<>'' then
+    CreateListItemBlock(Result,aListItemText);
+end;
+
+function TTestHTMLRender.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 TTestHTMLRender.TestRender(const aHTML: string);
+
+var
+  L : TStrings;
+
+begin
+  L:=TstringList.Create;
+  try
+    L.SkipLastLineBreak:=True;
+    Renderer.RenderDocument(FDocument,L);
+    assertEquals('Correct html: ',aHTML,L.Text);
+  finally
+    L.Free;
+  end;
+end;
+
+
+procedure TTestHTMLRender.TestHookup;
+
+begin
+  AssertNotNull('Have renderer',FHTMLRenderer);
+  AssertNotNull('Have document',FDocument);
+  AssertEquals('Have empty document',0,FDocument.blocks.Count);
+end;
+
+
+procedure TTestHTMLRender.TestEmpty;
+
+begin
+  Renderer.Options:=[hoEnvelope];
+  TestRender('<!DOCTYPE html>'+sLineBreak+'<html>'+sLineBreak+'<body>'+sLineBreak+'</body>'+sLineBreak+'</html>');
+end;
+
+
+procedure TTestHTMLRender.TestEmptyNoEnvelope;
+
+begin
+  Renderer.Options:=[];
+  TestRender('');
+end;
+
+
+procedure TTestHTMLRender.TestEmptyTitle;
+
+begin
+  Renderer.Options:=[hoEnvelope,hoHead];
+  Renderer.Title:='a';
+  TestRender('<!DOCTYPE html>'+sLineBreak+'<html>'+sLineBreak
+             +'<head>'+sLineBreak+'<title>a</title>'+sLineBreak+'</head>'+sLineBreak
+             +'<body>'+sLineBreak+'</body>'+sLineBreak+'</html>');
+end;
+
+
+procedure TTestHTMLRender.TestEmptyHead;
+
+begin
+  Renderer.Options:=[hoEnvelope,hoHead];
+  Renderer.Head.Add('<meta charset="UTF8">');
+  TestRender('<!DOCTYPE html>'+sLineBreak+'<html>'+sLineBreak
+             +'<head>'+sLineBreak+'<meta charset="UTF8">'+sLineBreak+'</head>'+sLineBreak
+             +'<body>'+sLineBreak+'</body>'+sLineBreak+'</html>');
+end;
+
+procedure TTestHTMLRender.TestTextBlockEmpty;
+
+begin
+  CreateTextBlock(Document,'a','');
+  TestRender('');
+end;
+
+
+procedure TTestHTMLRender.TestTextBlockText;
+
+begin
+  CreateTextBlock(Document,'a','a');
+  TestRender('a');
+end;
+
+
+procedure TTestHTMLRender.TestTextBlockTextStrong;
+
+begin
+  CreateTextBlock(Document,'a','a',[nsStrong]);
+  TestRender('<b>a</b>');
+end;
+
+
+procedure TTestHTMLRender.TestTextBlockTextEmph;
+
+begin
+  CreateTextBlock(Document,'a','a',[nsEmph]);
+  TestRender('<i>a</i>');
+end;
+
+
+procedure TTestHTMLRender.TestTextBlockTextDelete;
+
+begin
+  CreateTextBlock(Document,'a','a',[nsDelete]);
+  TestRender('<del>a</del>');
+end;
+
+
+procedure TTestHTMLRender.TestTextBlockTextStrongEmph;
+
+begin
+  CreateTextBlock(Document,'a','a',[nsStrong,nsEmph]);
+  TestRender('<b><i>a</i></b>');
+end;
+
+
+procedure TTestHTMLRender.TestTextBlockTextStrongEmphSplit1;
+
+var
+  lBlock : TMarkDownTextBlock;
+
+begin
+  lBlock:=CreateTextBlock(Document,'a','a ',[nsStrong]);
+  AppendTextNode(lBlock,'b',[nsStrong,nsemph]);
+  TestRender('<b>a <i>b</i></b>');
+end;
+
+
+procedure TTestHTMLRender.TestTextBlockTextStrongEmphSplit2;
+
+var
+  lBlock : TMarkDownTextBlock;
+begin
+  lBlock:=CreateTextBlock(Document,'a','a',[nsEmph,nsStrong]);
+  AppendTextNode(lBlock,' b',[nsStrong]);
+  TestRender('<b><i>a</i> b</b>');
+end;
+
+
+procedure TTestHTMLRender.TestPragraphBlockEmpty;
+
+begin
+  CreateParagraphBlock('');
+  TestRender('<p></p>');
+end;
+
+
+procedure TTestHTMLRender.TestPragraphBlockText;
+
+begin
+  CreateParagraphBlock('a');
+  TestRender('<p>a</p>');
+end;
+
+
+procedure TTestHTMLRender.TestQuotedBlockEmpty;
+
+begin
+  CreateQuotedBlock('');
+  TestRender('<blockquote>'+sLineBreak+'</blockquote>');
+end;
+
+
+procedure TTestHTMLRender.TestQuotedBlockText;
+
+begin
+  CreateQuotedBlock('a');
+  TestRender('<blockquote>'+sLineBreak+'a</blockquote>');
+end;
+
+
+procedure TTestHTMLRender.TestHeadingBlockEmpty;
+
+begin
+  CreateHeadingBlock('',1);
+  TestRender('<h1></h1>');
+end;
+
+
+procedure TTestHTMLRender.TestHeadingBlockText;
+
+begin
+  CreateHeadingBlock('a',1);
+  TestRender('<h1>a</h1>');
+end;
+
+
+procedure TTestHTMLRender.TestHeadingBlockTextLevel2;
+
+begin
+  CreateHeadingBlock('a',2);
+  TestRender('<h2>a</h2>');
+end;
+
+
+procedure TTestHTMLRender.TestUnorderedListEmpty;
+
+begin
+  CreateListBlock(false,'');
+  TestRender('<ul>'+sLineBreak+'</ul>');
+end;
+
+
+procedure TTestHTMLRender.TestUnorderedListOneItem;
+
+begin
+  CreateListBlock(false,'a');
+  TestRender('<ul>'+sLineBreak+'<li>a</li>'+sLineBreak+'</ul>');
+end;
+
+
+initialization
+  Registertest(TTestHTMLRender);
+end.
+

+ 285 - 0
packages/fcl-md/tests/utest.markdown.inlinetext.pas

@@ -0,0 +1,285 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown Inline text processing 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.InlineText;
+
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, fpcunit, testregistry, contnrs,
+  Markdown.Elements, Markdown.Scanner, Markdown.InlineText;
+
+type
+
+  { TTestInlineTextProcessor }
+
+  TTestInlineTextProcessor = class(TTestCase)
+  private
+    FScanner: TMarkDownTextScanner;
+    FNodes: TMarkDownTextNodeList;
+    FProcessor: TInlineTextProcessor;
+    FEntities: TFPStringHashTable;
+
+    procedure DumpNodes;
+    procedure SetupProcessor(const AText: AnsiString; awsMode: TWhitespaceMode = wsTrim);
+    function NodeAsText(AIndex: Integer): TMarkDownTextNode;
+    function NodeAsNamed(AIndex: Integer; AExpectedKind: TTextNodeKind): TMarkDownTextNode;
+    class procedure AssertEquals(const aMsg: string; aExpected,aActual : TTextNodeKind); overload;
+    class procedure AssertEquals(const aMsg: string; aExpected,aActual : TNodeStyle); overload;
+    class procedure AssertEquals(const aMsg: string; aExpected,aActual : TNodeStyles); overload;
+
+  protected
+    procedure TearDown; override;
+  published
+    procedure TestSimpleText;
+    procedure TestBackslashEscapes;
+    procedure TestCodeSpans;
+    procedure TestEmphasisAndStrong;
+    procedure TestEmphasisAndStrongInOne;
+    procedure TestEmphasisAndStrongInOneSplit;
+    procedure TestEmphasisAndStrongInOneSplit2;
+    procedure TestStrikethroughGFM;
+    procedure TestAutoLinks;
+    procedure TestInlineLink;
+    procedure TestInlineImage;
+  end;
+
+implementation
+
+uses typinfo;
+
+{ TTestInlineTextProcessor }
+
+procedure TTestInlineTextProcessor.TearDown;
+begin
+  FProcessor.Free;
+  FScanner.Free;
+  FNodes.Free;
+  FEntities.Free;
+end;
+
+procedure TTestInlineTextProcessor.SetupProcessor(const AText: AnsiString; awsMode: TWhitespaceMode = wsTrim);
+begin
+  FScanner := TMarkDownTextScanner.Create(AText, 1);
+  FNodes := TMarkDownTextNodeList.Create(True);
+  FEntities := TFPStringHashTable.Create; // Assuming no custom entities for now
+  FProcessor := TInlineTextProcessor.Create(FScanner, FNodes, FEntities, awsMode);
+end;
+
+function TTestInlineTextProcessor.NodeAsText(AIndex: Integer): TMarkDownTextNode;
+begin
+  AssertEquals('Node at index ' + IntToStr(AIndex) + ' should be text', nkText, FNodes[AIndex].Kind);
+  Result := FNodes[AIndex];
+end;
+
+function TTestInlineTextProcessor.NodeAsNamed(AIndex: Integer; AExpectedKind: TTextNodeKind): TMarkDownTextNode;
+begin
+  AssertEquals('Node at index ' + IntToStr(AIndex) + ' should have kind ' + GetEnumName(TypeInfo(TTextNodeKind), Ord(AExpectedKind)), AExpectedKind, FNodes[AIndex].Kind);
+  Result := FNodes[AIndex];
+end;
+
+class procedure TTestInlineTextProcessor.AssertEquals(const aMsg: string; aExpected, aActual: TTextNodeKind);
+begin
+  AssertEquals(aMsg,GetEnumName(TypeInfo(TTextNodeKind),Ord(aExpected)),GetEnumName(TypeInfo(TTextNodeKind),Ord(aActual)));
+end;
+
+class procedure TTestInlineTextProcessor.AssertEquals(const aMsg: string; aExpected, aActual: TNodeStyle);
+begin
+  AssertEquals(aMsg,GetEnumName(TypeInfo(TNodeStyle),Ord(aExpected)),GetEnumName(TypeInfo(TNodeStyle),Ord(aActual)));
+end;
+
+class procedure TTestInlineTextProcessor.AssertEquals(const aMsg: string; aExpected, aActual: TNodeStyles);
+begin
+  AssertEquals(aMsg,SetToString(PTypeInfo(TypeInfo(TNodeStyles)),Integer(aExpected),False),
+                    SetToString(PTypeInfo(TypeInfo(TNodeStyles)),Integer(aActual),False));
+end;
+
+procedure TTestInlineTextProcessor.TestSimpleText;
+begin
+  SetupProcessor('This is a simple text.');
+  FProcessor.Process(True);
+  AssertEquals('Should have one text node', 1, FNodes.Count);
+  AssertEquals('Text content mismatch', 'This is a simple text.', NodeAsText(0).NodeText);
+end;
+
+procedure TTestInlineTextProcessor.TestBackslashEscapes;
+begin
+  SetupProcessor('This is \*not\* emphasis.');
+  FProcessor.Process(True);
+  AssertEquals('Should have one text node for escaped chars', 1, FNodes.Count);
+  AssertEquals('Escaped character was not handled correctly', 'This is *not* emphasis.', NodeAsText(0).NodeText);
+end;
+
+procedure TTestInlineTextProcessor.DumpNodes;
+
+begin
+  FProcessor.DumpNodes;
+end;
+
+procedure TTestInlineTextProcessor.TestCodeSpans;
+var
+  Node: TMarkDownTextNode;
+begin
+  SetupProcessor('Use the `printf()` function.');
+  FProcessor.Process(True);
+  AssertEquals('Should have 3 nodes for a code span', 3, FNodes.Count);
+  AssertEquals('Text before code span', 'Use the ', NodeAsText(0).NodeText);
+  Node:=NodeAsNamed(1,nkCode);
+  AssertEquals('Code span content', 'printf()', Node.NodeText);
+  AssertEquals('Text after code span', ' function.', NodeAsText(2).NodeText);
+end;
+
+procedure TTestInlineTextProcessor.TestEmphasisAndStrong;
+var
+  Node: TMarkDownTextNode;
+begin
+  SetupProcessor('*emphasis* and **strong**');
+  FProcessor.Process(True);
+  AssertEquals('Should have 3 nodes for emphasis and strong', 3, FNodes.Count);
+  // *emphasis*
+  Node := NodeAsText(0);
+  AssertEquals('Emphasis content', 'emphasis', Node.NodeText);
+  AssertEquals('Style', [nsEmph], Node.Styles);
+
+  Node:=NodeAsText(1);
+  AssertEquals('Connector text', ' and ', Node.NodeText);
+  AssertEquals('Style', [], Node.Styles);
+
+  // **strong**
+  Node := NodeAsText(2);
+  AssertEquals('Style', [nsStrong], Node.Styles);
+  AssertEquals('Strong content', 'strong', Node.NodeText);
+end;
+
+procedure TTestInlineTextProcessor.TestEmphasisAndStrongInOne;
+var
+  Node: TMarkDownTextNode;
+begin
+  SetupProcessor('***emphasis and strong***');
+  FProcessor.Process(True);
+  AssertEquals('Should have 1 node for emphasis and strong', 1, FNodes.Count);
+  Node := NodeAsText(0);
+  AssertEquals('Emphasis content', 'emphasis and strong', Node.NodeText);
+  AssertEquals('Style', [nsStrong,nsEmph], Node.Styles);
+end;
+
+procedure TTestInlineTextProcessor.TestEmphasisAndStrongInOneSplit;
+var
+  Node: TMarkDownTextNode;
+begin
+  SetupProcessor('***strong** and emphasis*');
+  FProcessor.Process(True);
+  AssertEquals('Should have 2 nodes for emphasis and strong', 2, FNodes.Count);
+  Node := NodeAsText(0);
+  AssertEquals('content', 'strong', Node.NodeText);
+  AssertEquals('Style', [nsStrong,nsEmph], Node.Styles);
+  Node := NodeAsText(1);
+  AssertEquals('Emphasis content', ' and emphasis', Node.NodeText);
+  AssertEquals('Style', [nsEmph], Node.Styles);
+end;
+
+procedure TTestInlineTextProcessor.TestEmphasisAndStrongInOneSplit2;
+var
+  Node: TMarkDownTextNode;
+begin
+  SetupProcessor('*emphasis and **strong***');
+  FProcessor.Process(True);
+  AssertEquals('Should have 2 nodes for emphasis and strong', 2, FNodes.Count);
+  Node := NodeAsText(0);
+  AssertEquals('content', 'emphasis and ', Node.NodeText);
+  AssertEquals('Style', [nsEmph], Node.Styles);
+  Node := NodeAsText(1);
+  AssertEquals('Emphasis content', 'strong', Node.NodeText);
+  AssertEquals('Style', [nsStrong,nsEmph], Node.Styles);
+end;
+
+procedure TTestInlineTextProcessor.TestStrikethroughGFM;
+var
+  Node: TMarkDownTextNode;
+begin
+  SetupProcessor('This is ~~deleted~~ text.');
+  FProcessor.GFMExtensions := True;
+  FProcessor.Process(True);
+
+  AssertEquals('Should have 3 nodes for strikethrough', 3, FNodes.Count);
+  AssertEquals('Text before', 'This is ', NodeAsText(0).NodeText);
+
+  Node := NodeAsText(1);
+  AssertEquals('Strikethrough style', [nsDelete], Node.Styles);
+  AssertEquals('Strikethrough content', 'deleted', Node.NodeText);
+
+  Node := NodeAsText(2);
+  AssertEquals('Text after', ' text.', Node.NodeText);
+end;
+
+procedure TTestInlineTextProcessor.TestAutoLinks;
+var
+  Node: TMarkDownTextNode;
+begin
+  SetupProcessor('See <https://www.example.com>.');
+  FProcessor.Process(True);
+
+  AssertEquals('Should have 3 nodes for autolink', 3, FNodes.Count);
+  AssertEquals('Text before', 'See ', NodeAsText(0).NodeText);
+
+  Node := NodeAsNamed(1, nkURI);
+  AssertEquals('href attribute', 'https://www.example.com', Node.attrs['href']);
+  AssertEquals('Autolink text content', 'https://www.example.com', Node.NodeText);
+
+  AssertEquals('Text after', '.', NodeAsText(2).NodeText);
+end;
+
+procedure TTestInlineTextProcessor.TestInlineLink;
+var
+  Node: TMarkDownTextNode;
+begin
+  SetupProcessor('A [link](https://example.com "Title").');
+  FProcessor.Process(True);
+  AssertEquals('Should have 3 nodes for inline link', 3, FNodes.Count);
+  AssertEquals('Text before', 'A ', NodeAsText(0).NodeText);
+
+  Node := NodeAsNamed(1, nkURI);
+  AssertEquals('Link href', 'https://example.com', Node.attrs['href']);
+  AssertEquals('Link title', 'Title', Node.attrs['title']);
+  AssertEquals('Link text content', 'link', Node.NodeText);
+  Node := NodeAsNamed(2, nkText);
+  AssertEquals('Final text content', '.', Node.NodeText);
+end;
+
+procedure TTestInlineTextProcessor.TestInlineImage;
+var
+  Node: TMarkDownTextNode;
+begin
+  SetupProcessor('An image ![alt text](/path/img.jpg "title").');
+  FProcessor.Process(True);
+  AssertEquals('Should have 3 nodes for inline image', 3, FNodes.Count);
+  Node:=NodeAsText(0);
+  AssertEquals('Text before', 'An image ', Node.NodeText);
+  Node := NodeAsNamed(1, nkImg);
+  AssertEquals('Image src', '/path/img.jpg', Node.attrs['src']);
+  AssertEquals('Image alt', 'alt text', Node.attrs['alt']);
+  AssertEquals('Image title', 'title', Node.attrs['title']);
+  Node:=NodeAsText(2);
+  AssertEquals('Text after', '.', Node.NodeText);
+end;
+
+initialization
+  RegisterTest(TTestInlineTextProcessor);
+end.
+
+

+ 399 - 0
packages/fcl-md/tests/utest.markdown.parser.pas

@@ -0,0 +1,399 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown block parser 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.Parser;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, fpcunit, testregistry, Contnrs,
+  Markdown.Elements, Markdown.Parser;
+
+type
+  { TBlockTestCase }
+  // Helper base class to avoid boilerplate code
+  TBlockTestCase = class(TTestCase)
+  private
+    FDoc: TMarkDownDocument;
+    FParser: TMarkDownParser;
+    FStrings: TStringList;
+    procedure CheckTextnodeText(const aMsg: string; aBlock: TMarkDownBlock; const aText: string);
+  protected
+    procedure SetupParser(const AText: String);
+    procedure CheckBlockText(const aMsg: string; aBlock: TMarkDownBlock; const aText : string; aInParagraph: Boolean);
+    function GetBlock(AIndex: Integer): TMarkDownBlock;
+    property Doc: TMarkDownDocument read FDoc;
+  public
+    procedure SetUp; override;
+    procedure TearDown; override;
+  end;
+
+  { TTestParagraphs }
+  TTestParagraphs = class(TBlockTestCase)
+  published
+    procedure TestSimpleParagraph;
+    procedure TestMultipleParagraphs;
+  end;
+
+  { TTestHeadings }
+  TTestHeadings = class(TBlockTestCase)
+  published
+    procedure TestATXHeading;
+    procedure TestSetextHeadings;
+  end;
+
+  { TTestCodeBlocks }
+  TTestCodeBlocks = class(TBlockTestCase)
+  published
+    procedure TestIndentedCodeBlock;
+    procedure TestFencedCodeBlock;
+    procedure TestFencedCodeBlockWithInfoString;
+  end;
+
+  { TTestBlockQuotes }
+  TTestBlockQuotes = class(TBlockTestCase)
+  published
+    procedure TestSimpleQuote;
+    procedure TestNestedQuote;
+    procedure TestLazy;
+  end;
+
+  { TTestLists }
+  TTestLists = class(TBlockTestCase)
+  published
+    procedure TestUnorderedList;
+    procedure TestOrderedList;
+    procedure TestNestedList;
+  end;
+
+  { TTestThematicBreaks }
+  TTestThematicBreaks = class(TBlockTestCase)
+  published
+    procedure TestAsteriskBreak;
+    procedure TestUnderscoreBreak;
+  end;
+
+  { TTestTables }
+  TTestTables = class(TBlockTestCase)
+  published
+    procedure TestSimpleTable;
+  end;
+
+implementation
+
+{ TBlockTestCase }
+
+procedure TBlockTestCase.SetUp;
+begin
+  inherited SetUp;
+  FStrings := TStringList.Create;
+  FParser := TMarkDownParser.Create(nil);
+end;
+
+procedure TBlockTestCase.TearDown;
+begin
+  FDoc.Free;
+  FParser.Free;
+  FStrings.Free;
+  inherited TearDown;
+end;
+
+procedure TBlockTestCase.SetupParser(const AText: String);
+
+begin
+  FStrings.Text := AText;
+  FDoc := FParser.Parse(FStrings);
+//  FDoc.Dump('');
+  AssertNotNull('Document should be parsed', FDoc);
+end;
+
+procedure TBlockTestCase.CheckBlockText(Const aMsg : string; aBlock: TMarkDownBlock; const aText : String; aInParagraph: Boolean);
+var
+  lBlock : TMarkDownBlock;
+begin
+  lBlock:=aBlock;
+  AssertTrue(aMsg+': Have child',lBlock.ChildCount>0);
+  if aInParagraph then
+    begin
+    lBlock:=lBlock[0];
+    AssertEquals(aMsg+': child is para',TMarkDownParagraphBlock,lBlock.ClassType);
+    AssertTrue(aMsg+': Paragrapg Has child',lBlock.ChildCount>0);
+    end;
+  lBlock:=lBlock[0];
+  CheckTextnodeText(aMsg,lBlock,aText);
+end;
+
+procedure TBlockTestCase.CheckTextnodeText(const aMsg : string; aBlock : TMarkDownBlock; const aText : string);
+
+var
+  lText : TMarkDownTextBlock absolute aBlock;
+  lTextNode : TMarkDownTextNode;
+  lCount : Integer;
+begin
+  AssertEquals(aMsg+': block is text',TMarkDownTextBlock,aBlock.ClassType);
+  lCount:=lText.Nodes.Count;
+  AssertTrue(aMsg+' text nodes',lCount>0);
+  lTextNode:=lText.Nodes[0];
+  AssertEquals(aMsg+' text node text',aText,lTextNode.NodeText);
+end;
+
+function TBlockTestCase.GetBlock(AIndex: Integer): TMarkDownBlock;
+begin
+  AssertTrue('Block index out of bounds', AIndex < FDoc.Blocks.Count);
+  Result := FDoc.Blocks[AIndex];
+end;
+
+{ TTestParagraphs }
+
+procedure TTestParagraphs.TestSimpleParagraph;
+var
+  Block: TMarkDownParagraphBlock;
+begin
+  SetupParser('This is a simple paragraph.');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  Block := GetBlock(0) as TMarkDownParagraphBlock;
+  AssertNotNull('Block should be a paragraph', Block);
+  AssertTrue('Should be a plain paragraph', Block.isPlainPara);
+end;
+
+procedure TTestParagraphs.TestMultipleParagraphs;
+begin
+  SetupParser('First paragraph.'#10#10'Second paragraph.');
+  AssertEquals('Document should have 2 blocks', 2, Doc.Blocks.Count);
+  AssertTrue('First block should be a paragraph', GetBlock(0) is TMarkDownParagraphBlock);
+  AssertTrue('Second block should be a paragraph', GetBlock(1) is TMarkDownParagraphBlock);
+end;
+
+{ TTestHeadings }
+
+procedure TTestHeadings.TestATXHeading;
+var
+  Block: TMarkDownHeadingBlock;
+begin
+  SetupParser('# A Level 1 Heading');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  Block := GetBlock(0) as TMarkDownHeadingBlock;
+  AssertNotNull('Block should be a heading', Block);
+  AssertEquals('Heading level should be 1', 1, Block.Level);
+end;
+
+procedure TTestHeadings.TestSetextHeadings;
+var
+  Block: TMarkDownParagraphBlock;
+begin
+  SetupParser('A Level 2 Heading'#10'-----------------');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  Block := GetBlock(0) as TMarkDownParagraphBlock;
+  AssertNotNull('Block should be a paragraph (used for setext)', Block);
+  AssertEquals('Header property should be 2 for setext', 2, Block.Header);
+end;
+
+{ TTestCodeBlocks }
+
+procedure TTestCodeBlocks.TestIndentedCodeBlock;
+var
+  Block: TMarkDownCodeBlock;
+begin
+  SetupParser('    a = 1;'#10'    b = 2;');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  Block := GetBlock(0) as TMarkDownCodeBlock;
+  AssertNotNull('Block should be a code block', Block);
+  AssertFalse('Should not be a fenced code block', Block.Fenced);
+end;
+
+procedure TTestCodeBlocks.TestFencedCodeBlock;
+var
+  Block: TMarkDownCodeBlock;
+begin
+  SetupParser('```'#10'code here'#10'```');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  Block := GetBlock(0) as TMarkDownCodeBlock;
+  AssertNotNull('Block should be a code block', Block);
+  AssertTrue('Should be a fenced code block', Block.Fenced);
+end;
+
+procedure TTestCodeBlocks.TestFencedCodeBlockWithInfoString;
+var
+  Block: TMarkDownCodeBlock;
+begin
+  SetupParser('~~~ pascal'#10'var i: Integer;'#10'~~~');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  Block := GetBlock(0) as TMarkDownCodeBlock;
+  AssertNotNull('Block should be a code block', Block);
+  AssertTrue('Should be a fenced code block', Block.Fenced);
+  AssertEquals('Language info string incorrect', 'pascal', Block.Lang);
+end;
+
+{ TTestBlockQuotes }
+
+procedure TTestBlockQuotes.TestSimpleQuote;
+var
+  Block: TMarkDownQuoteBlock;
+begin
+  SetupParser('> This is a quote.');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  Block := GetBlock(0) as TMarkDownQuoteBlock;
+  AssertNotNull('Block should be a quote block', Block);
+end;
+
+procedure TTestBlockQuotes.TestNestedQuote;
+var
+  OuterQuote, InnerQuote: TMarkDownQuoteBlock;
+begin
+  SetupParser('> First level'#10'>> Second level');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  AssertEquals('Outer block should be a quote', TMarkDownQuoteBlock,GetBlock(0).ClassType);
+  OuterQuote :=GetBlock(0)  as TMarkDownQuoteBlock;
+  AssertEquals('Outer quote should have 2 blocks inside', 2, OuterQuote.Blocks.Count); // Para and another quote
+  AssertEquals('First inner block is a paragraph', TMarkDownParagraphBlock,OuterQuote.Blocks[0].ClassType);
+  AssertEquals('Second inner block should be a quote', TMarkDownQuoteBlock,OuterQuote.Blocks[1].ClassType);
+  InnerQuote :=OuterQuote.Blocks[1] as TMarkDownQuoteBlock;
+  AssertEquals('Outer quote should have 1 block inside', 1, InnerQuote.Blocks.Count); // Para and another quote
+  AssertEquals('First inner block is a paragraph', TMarkDownParagraphBlock,InnerQuote.Blocks[0].ClassType);
+end;
+
+procedure TTestBlockQuotes.TestLazy;
+var
+  OuterQuote: TMarkDownQuoteBlock;
+begin
+  SetupParser('> First level'#10'Continues');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  AssertEquals('Outer block should be a quote', TMarkDownQuoteBlock,GetBlock(0).ClassType);
+  OuterQuote :=GetBlock(0)  as TMarkDownQuoteBlock;
+  AssertEquals('Outer quote should have 1 blocks inside', 1, OuterQuote.Blocks.Count); // Para and another quote
+  AssertEquals('First inner block is a paragraph', TMarkDownParagraphBlock,OuterQuote.Blocks[0].ClassType);
+end;
+
+{ TTestLists }
+
+procedure TTestLists.TestUnorderedList;
+var
+  List: TMarkDownListBlock;
+  ListItem: TMarkDownListItemBlock;
+begin
+  SetupParser('* Item 1'#10'* Item 2');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  List := GetBlock(0) as TMarkDownListBlock;
+  AssertNotNull('Block should be a list', List);
+  AssertFalse('List should be unordered', List.Ordered);
+  AssertEquals('List should have 2 items', 2, List.Blocks.Count);
+  // Check first list item and its contents
+  AssertTrue('First item should be a list item block', List.Blocks[0] is TMarkDownListItemBlock);
+  ListItem := List.Blocks[0] as TMarkDownListItemBlock;
+  AssertEquals('First list item should contain one inner block', 1, ListItem.Blocks.Count);
+  AssertTrue('Inner block of first list item should be a paragraph', ListItem.Blocks[0] is TMarkDownParagraphBlock);
+  CheckBlockText('First block',ListItem,'Item 1',True);
+  // Check second list item and its contents
+  AssertTrue('Second item should be a list item block', List.Blocks[1] is TMarkDownListItemBlock);
+  ListItem := List.Blocks[1] as TMarkDownListItemBlock;
+  AssertEquals('Second list item should contain one inner block', 1, ListItem.Blocks.Count);
+  AssertTrue('Inner block of second list item should be a paragraph', ListItem.Blocks[0] is TMarkDownParagraphBlock);
+  CheckBlockText('Second block',ListItem,'Item 2',True);
+end;
+
+procedure TTestLists.TestOrderedList;
+var
+  List: TMarkDownListBlock;
+  ListItem: TMarkDownListItemBlock;
+begin
+  SetupParser('1. First item'#10'2. Second item');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  List := GetBlock(0) as TMarkDownListBlock;
+  AssertNotNull('Block should be a list', List);
+  AssertTrue('List should be ordered', List.Ordered);
+  AssertEquals('List should have 2 items', 2, List.Blocks.Count);
+  ListItem := List.Blocks[0] as TMarkDownListItemBlock;
+  AssertEquals('First list item should contain one inner block', 1, ListItem.Blocks.Count);
+  AssertTrue('Inner block of first list item should be a paragraph', ListItem.Blocks[0] is TMarkDownParagraphBlock);
+  CheckBlockText('First block',ListItem,'First item',True);
+  ListItem := List.Blocks[1] as TMarkDownListItemBlock;
+  AssertEquals('Second list item should contain one inner block', 1, ListItem.Blocks.Count);
+  AssertTrue('Inner block of second list item should be a paragraph', ListItem.Blocks[0] is TMarkDownParagraphBlock);
+  CheckBlockText('First block',ListItem,'Second item',True);
+end;
+
+procedure TTestLists.TestNestedList;
+var
+  OuterList, InnerList: TMarkDownListBlock;
+  OuterItem: TMarkDownListItemBlock;
+begin
+  SetupParser('* Level 1'#10'  * Level 2');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  OuterList := GetBlock(0) as TMarkDownListBlock;
+  AssertNotNull('Outer block should be a list', OuterList);
+  AssertEquals('Outer list should have 1 item', 1, OuterList.Blocks.Count);
+
+  OuterItem := OuterList.Blocks[0] as TMarkDownListItemBlock;
+  AssertEquals('Outer item should contain 2 blocks (para, list)', 2, OuterItem.Blocks.Count);
+
+  InnerList := OuterItem.Blocks[1] as TMarkDownListBlock;
+  AssertNotNull('Inner block should be a list', InnerList);
+end;
+
+{ TTestThematicBreaks }
+
+procedure TTestThematicBreaks.TestAsteriskBreak;
+begin
+  SetupParser('***');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  AssertTrue('Block should be a thematic break', GetBlock(0) is TMarkDownThematicBreakBlock);
+end;
+
+procedure TTestThematicBreaks.TestUnderscoreBreak;
+begin
+  SetupParser('---');
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  AssertTrue('Block should be a thematic break', GetBlock(0) is TMarkDownThematicBreakBlock);
+end;
+
+{ TTestTables }
+
+procedure TTestTables.TestSimpleTable;
+var
+  Table: TMarkDownTableBlock;
+  HeaderRow, BodyRow: TMarkDownTableRowBlock;
+begin
+  SetupParser(
+    '| Header 1 | Header 2 |'#10 +
+    '|----------|----------|'#10 +
+    '| Cell 1   | Cell 2   |'
+  );
+  AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
+  Table := GetBlock(0) as TMarkDownTableBlock;
+  AssertNotNull('Block should be a table', Table);
+  AssertEquals('Table should have 2 rows', 2, Table.Blocks.Count);
+  AssertEquals('Table should have 2 columns', 2, Length(Table.Columns));
+
+  HeaderRow := Table.Blocks[0] as TMarkDownTableRowBlock;
+  AssertNotNull('First row should be a table row', HeaderRow);
+  AssertEquals('Header row should have 2 cells', 2, HeaderRow.Blocks.Count);
+  CheckTextnodeText('Header row, Cell 1',HeaderRow.Blocks[0],'Header 1');
+  CheckTextnodeText('Header row, Cell 2',HeaderRow.Blocks[1],'Header 2');
+
+  BodyRow := Table.Blocks[1] as TMarkDownTableRowBlock;
+  AssertNotNull('Second row should be a table row', BodyRow);
+  AssertEquals('Body row should have 2 cells', 2, BodyRow.Blocks.Count);
+  CheckTextnodeText('Body Row 1, Cell 1',BodyRow.Blocks[0],'Cell 1');
+  CheckTextnodeText('Body Row 1, Cell 2',BodyRow.Blocks[1],'Cell 2');
+end;
+
+
+initialization
+  RegisterTests('Parser',[TTestParagraphs, TTestHeadings, TTestCodeBlocks,
+                          TTestBlockQuotes, TTestLists, TTestThematicBreaks,
+                          TTestTables]);
+end.
+

+ 281 - 0
packages/fcl-md/tests/utest.markdown.scanner.pas

@@ -0,0 +1,281 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown text scanner 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.Scanner;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, fpcunit, testregistry,
+  MarkDown.Elements, MarkDown.Scanner;
+
+type
+  TMyMarkDownTextScanner = Class(TMarkDownTextScanner)
+  Public
+    Property Cursor;
+    Property LineNo;
+  end;
+
+  { TTestMarkDownTextScanner }
+
+  TTestMarkDownTextScanner = class(TTestCase)
+  private
+    FScanner: TMyMarkDownTextScanner;
+  protected
+    procedure InitScanner(const aText : string; aLineNo : Integer);
+    procedure SetUp; override;
+    procedure TearDown; override;
+    property Scanner : TMyMarkDownTextScanner Read FScanner;
+  published
+    procedure TestCreate;
+    procedure TestEOF;
+    procedure TestPeek;
+    procedure TestPeekNext;
+    procedure TestPeekPrevious;
+    procedure TestPeekEndRun;
+    procedure TestNextChar;
+    procedure TestLocation;
+    procedure TestBookmark;
+    procedure TestPeekLen;
+    procedure TestPeekWhile;
+    procedure TestPeekUntil;
+    procedure TestPeekRun;
+    procedure TestNextChars;
+    procedure TestNextEquals;
+    procedure TestHas;
+    procedure TestSkipWhitespace;
+    procedure TestFindMatchingOccurrence;
+    procedure TestFindMatchingOccurrenceExclude;
+  end;
+
+implementation
+
+{ TTestMarkDownTextScanner }
+
+procedure TTestMarkDownTextScanner.InitScanner(const aText: string; aLineNo: Integer);
+begin
+  FreeAndNil(FScanner);
+  FScanner:=TMyMarkDownTextScanner.Create(aText, aLineNo);
+end;
+
+procedure TTestMarkDownTextScanner.SetUp;
+begin
+  InitScanner('abcde', 1);
+end;
+
+procedure TTestMarkDownTextScanner.TearDown;
+begin
+  FreeAndNil(FScanner);
+end;
+
+procedure TTestMarkDownTextScanner.TestCreate;
+begin
+  AssertNotNull('Scanner should be created', FScanner);
+  AssertEquals('Initial cursor should be 1', 1, FScanner.Cursor);
+  AssertEquals('Initial line number should be set', 1, FScanner.LineNo);
+end;
+
+procedure TTestMarkDownTextScanner.TestEOF;
+
+begin
+  // Test with the default scanner
+  AssertFalse('Should not be EOF at start', FScanner.EOF);
+  FScanner.NextChars(5);
+  AssertTrue('Should be EOF after reading all chars', FScanner.EOF);
+
+  // Test with an empty string
+  InitScanner('', 1);
+  AssertTrue('Empty scanner should be at EOF', Scanner.EOF);
+end;
+
+procedure TTestMarkDownTextScanner.TestPeek;
+begin
+  AssertEquals('Peek should return first char', 'a', FScanner.Peek);
+  FScanner.NextChar;
+  AssertEquals('Peek should return second char', 'b', FScanner.Peek);
+  FScanner.NextChars(4); // Move to end
+  AssertTrue('Should be at EOF', FScanner.EOF);
+  AssertEquals('Peek at EOF should return #0', #0, FScanner.Peek);
+end;
+
+procedure TTestMarkDownTextScanner.TestPeekNext;
+begin
+  AssertEquals('PeekNext should see the second char', 'b', FScanner.PeekNext);
+  FScanner.NextChars(3);
+  AssertEquals('PeekNext should see the last char', 'e', FScanner.PeekNext);
+  FScanner.NextChar;
+  AssertEquals('PeekNext at the last char should be #0', #0, FScanner.PeekNext);
+  FScanner.NextChar;
+  AssertTrue('Should be at EOF', FScanner.EOF);
+  AssertEquals('PeekNext at EOF should be #0', #0, FScanner.PeekNext);
+end;
+
+procedure TTestMarkDownTextScanner.TestPeekPrevious;
+begin
+  AssertEquals('PeekPrevious at start should be #0', #0, FScanner.PeekPrevious);
+  FScanner.NextChar;
+  AssertEquals('PeekPrevious at second char should be "a"', 'a', FScanner.PeekPrevious);
+  FScanner.NextChars(4);
+  AssertEquals('PeekPrevious at EOF should be "e"', 'e', FScanner.PeekPrevious);
+end;
+
+procedure TTestMarkDownTextScanner.TestPeekEndRun;
+
+begin
+  InitScanner('a--b-c', 1);
+  AssertEquals('PeekEndRun on single char', '-', Scanner.PeekEndRun);
+  Scanner.NextChar; // Cursor at '-'
+  AssertEquals('PeekEndRun on run start', 'b', Scanner.PeekEndRun);
+  Scanner.NextChars(3); // Cursor at '-'
+  AssertEquals('PeekEndRun on single char before another', 'c', Scanner.PeekEndRun);
+  Scanner.NextChars(2); // Cursor at EOF
+  AssertEquals('PeekEndRun at EOF', #0, Scanner.PeekEndRun);
+end;
+
+procedure TTestMarkDownTextScanner.TestNextChar;
+begin
+  AssertEquals('NextChar should return "a"', 'a', FScanner.NextChar);
+  AssertEquals('Cursor should advance to 2', 2, FScanner.Cursor);
+  AssertEquals('Peek should now be "b"', 'b', FScanner.Peek);
+  FScanner.NextChars(4); // To EOF
+  AssertEquals('NextChar at EOF should return #0', #0, FScanner.NextChar);
+end;
+
+procedure TTestMarkDownTextScanner.TestLocation;
+var
+  Pos: TPosition;
+begin
+  InitScanner('a'#10'b', 5); // Start at line 5
+  Pos := Scanner.Location;
+  AssertEquals('Initial line should be 5', 5, Pos.Line);
+  AssertEquals('Initial col should be 1', 1, Pos.Col);
+
+  Scanner.NextChar; // consume 'a'
+  Pos := Scanner.Location;
+  AssertEquals('Line should still be 5', 5, Pos.Line);
+  AssertEquals('Col should be 2', 2, Pos.Col);
+
+  Scanner.NextChar; // consume #10
+  Pos := Scanner.Location;
+  AssertEquals('Line should be 6 after newline', 6, Pos.Line);
+  AssertEquals('Col should be 1 after newline', 1, Pos.Col);
+end;
+
+procedure TTestMarkDownTextScanner.TestBookmark;
+begin
+  FScanner.NextChars(2); // Cursor at 'c'
+  AssertEquals('Cursor should be at position 3', 3, FScanner.Cursor);
+  FScanner.Bookmark;
+  FScanner.NextChars(2); // Cursor at 'e'
+  AssertEquals('Cursor should be at position 5', 5, FScanner.Cursor);
+  FScanner.GotoBookmark;
+  AssertEquals('Cursor should be back at position 3', 3, FScanner.Cursor);
+  AssertEquals('Peek should be "c" after goto bookmark', 'c', FScanner.Peek);
+end;
+
+procedure TTestMarkDownTextScanner.TestPeekLen;
+begin
+  AssertEquals('PeekLen(3) should be "abc"', 'abc', FScanner.PeekLen(3));
+  AssertEquals('Cursor should not move after PeekLen', 1, FScanner.Cursor);
+  FScanner.NextChars(3);
+  AssertEquals('PeekLen(5) past EOF should be "de"', 'de', FScanner.PeekLen(5));
+  AssertEquals('Cursor should not move after PeekLen past EOF', 4, FScanner.Cursor);
+end;
+
+procedure TTestMarkDownTextScanner.TestPeekWhile;
+begin
+  AssertEquals('PeekWhile should get "ab"', 'ab', FScanner.PeekWhile(['a', 'b']));
+  AssertEquals('Cursor should not move after PeekWhile', 1, FScanner.Cursor);
+  AssertEquals('PeekWhile with no match should be empty', '', FScanner.PeekWhile(['x', 'y']));
+end;
+
+procedure TTestMarkDownTextScanner.TestPeekUntil;
+
+begin
+  InitScanner('abc*def', 1);
+  AssertEquals('PeekUntil should get "abc"', 'abc', Scanner.PeekUntil(['*']));
+  AssertEquals('Cursor should not move after PeekUntil', 1, Scanner.Cursor);
+  AssertEquals('PeekUntil with no match should be empty', '', Scanner.PeekUntil(['z']));
+end;
+
+procedure TTestMarkDownTextScanner.TestPeekRun;
+
+begin
+  InitScanner('a***b', 1);
+  Scanner.NextChar;
+  AssertEquals('PeekRun should find "***"', '***', Scanner.PeekRun(False));
+  AssertEquals('Cursor should not move after PeekRun', 2, Scanner.Cursor);
+
+  // Test checkBefore
+  AssertEquals('PeekRun with checkBefore=true and different prev char should succeed', '***', Scanner.PeekRun(True));
+  Scanner.NextChar; // cursor at second '*'
+  AssertEquals('PeekRun with checkBefore=true and same prev char should fail', '', Scanner.PeekRun(True));
+end;
+
+procedure TTestMarkDownTextScanner.TestNextChars;
+begin
+  AssertEquals('NextChars(3) should return "abc"', 'abc', FScanner.NextChars(3));
+  AssertEquals('Cursor should be at 4 after NextChars(3)', 4, FScanner.Cursor);
+  AssertEquals('NextChars(5) past EOF should return "de"', 'de', FScanner.NextChars(5));
+  AssertTrue('Should be at EOF after reading past end', FScanner.EOF);
+end;
+
+procedure TTestMarkDownTextScanner.TestNextEquals;
+
+begin
+  InitScanner('---abc', 1);
+  AssertEquals('NextEquals should consume "---"', '---', Scanner.NextEquals);
+  AssertEquals('Cursor should be at 4 after NextEquals', 4, Scanner.Cursor);
+  AssertEquals('Peek should be "a"', 'a', Scanner.Peek);
+end;
+
+procedure TTestMarkDownTextScanner.TestHas;
+begin
+  AssertTrue('Has("ab") should be true at start', FScanner.Has('ab'));
+  AssertFalse('Has("ac") should be false at start', FScanner.Has('ac'));
+  AssertEquals('Cursor should not move after Has', 1, FScanner.Cursor);
+end;
+
+procedure TTestMarkDownTextScanner.TestSkipWhitespace;
+
+begin
+  InitScanner('  ab', 1);
+  Scanner.SkipWhitespace;
+  AssertEquals('Cursor should be at 3 after skipping whitespace', 3, Scanner.Cursor);
+  AssertEquals('Peek should be "a"', 'a', Scanner.Peek);
+end;
+
+procedure TTestMarkDownTextScanner.TestFindMatchingOccurrence;
+
+begin
+  InitScanner('some **bold** text, not ***this***', 1);
+  AssertTrue('Should find "**" in " **bold** "', Scanner.FindMatchingOccurrence('**'));
+  Scanner.Cursor:=25;
+  AssertFalse('Should not find "**" in "***this***" because of surrounding *', Scanner.FindMatchingOccurrence('**'));
+end;
+
+procedure TTestMarkDownTextScanner.TestFindMatchingOccurrenceExclude;
+begin
+  InitScanner('a `code` and not a \`backtick`', 1);
+  AssertTrue('Should find `code`', Scanner.FindMatchingOccurrence('`', '\'));
+  Scanner.Cursor := 18; // Move cursor to "and not a "
+  AssertFalse('Should not find `backtick` because it is escaped by \', Scanner.FindMatchingOccurrence('`', '\'));
+end;
+
+initialization
+  RegisterTest(TTestMarkDownTextScanner);
+end.
+

+ 297 - 0
packages/fcl-md/tests/utest.markdown.utils.pas

@@ -0,0 +1,297 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2025 by Michael Van Canneyt
+
+    Markdown utils 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.Utils;
+
+{$mode objfpc}
+{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, fpcunit, testregistry, contnrs,
+  markdown.utils;
+
+type
+
+  { TTestMarkdownUtils }
+
+  TTestMarkdownUtils = class(TTestCase)
+  private
+    FEntities: TFPStringHashTable;
+    FBuilder: TStringBuilder;
+  protected
+    procedure SetUp; override;
+    procedure TearDown; override;
+  published
+    procedure TestIsWhitespaceChar;
+    procedure TestIsWhitespace;
+    procedure TestMustEscape;
+    procedure TestIsStringOfChar;
+    procedure TestCopyUpTo;
+    procedure TestCopySkipped;
+    procedure TestCopyMatching;
+    procedure TestStartsWithWS;
+    procedure TestLeadingWhitespace;
+    procedure TestLengthWhiteSpaceCorrected;
+    procedure TestRemoveWS;
+    procedure TestStripWhitespace;
+    procedure TestHtmlEscape;
+    procedure TestUrlEscape;
+    procedure TestParseEntityString;
+    procedure TestCheckForEntity;
+    procedure TestIsRegexMatch;
+    procedure TestIsUnicodePunctuation;
+    procedure TestCountStartChars;
+    procedure TestToUnicodeChars;
+    procedure TestTransformTabs;
+  end;
+
+implementation
+
+procedure TTestMarkdownUtils.SetUp;
+begin
+  FEntities := TFPStringHashTable.Create;
+  FEntities.Add('amp', '&');
+  FEntities.Add('lt', '<');
+  FBuilder := TStringBuilder.Create;
+end;
+
+procedure TTestMarkdownUtils.TearDown;
+begin
+  FEntities.Free;
+  FBuilder.Free;
+end;
+
+procedure TTestMarkdownUtils.TestIsWhitespaceChar;
+begin
+  AssertTrue('Space should be whitespace', isWhitespaceChar(' '));
+  AssertTrue('Tab should be whitespace', isWhitespaceChar(#9));
+  AssertTrue('Line Feed should be whitespace', isWhitespaceChar(#10));
+  AssertFalse('Letter "a" should not be whitespace', isWhitespaceChar('a'));
+  AssertFalse('Carriage Return is not considered whitespace by this implementation', isWhitespaceChar(#13));
+end;
+
+procedure TTestMarkdownUtils.TestIsWhitespace;
+begin
+  AssertTrue('Empty string is considered whitespace', isWhitespace(''));
+  AssertTrue('String with only spaces is whitespace', isWhitespace(' '));
+  AssertTrue('String with mixed whitespace is whitespace', isWhitespace(#9#10' '));
+  AssertFalse('String with non-whitespace characters is not whitespace', isWhitespace(' a '));
+end;
+
+procedure TTestMarkdownUtils.TestMustEscape;
+begin
+  AssertTrue('! must be escaped', MustEscape('!'));
+  AssertTrue('& must be escaped', MustEscape('&'));
+  AssertTrue('\ must be escaped', MustEscape('\'));
+  AssertTrue('` must be escaped', MustEscape('`'));
+  AssertFalse('a must not be escaped', MustEscape('a'));
+  AssertFalse('1 must not be escaped', MustEscape('1'));
+  AssertFalse('space must not be escaped', MustEscape(' '));
+end;
+
+procedure TTestMarkdownUtils.TestIsStringOfChar;
+begin
+  AssertTrue('Empty string', IsStringOfChar(''));
+  AssertTrue('Single character string', IsStringOfChar('a'));
+  AssertTrue('String of identical chars', IsStringOfChar('---'));
+  AssertFalse('String of non-identical chars', IsStringOfChar('--a'));
+end;
+
+procedure TTestMarkdownUtils.TestCopyUpTo;
+begin
+  AssertEquals('Stop at first excluded char', 'abc', CopyUpTo('abc#def', ['#', ';']));
+  AssertEquals('No excluded chars present', 'abcdef', CopyUpTo('abcdef', ['#', ';']));
+  AssertEquals('Excluded char at start', '', CopyUpTo('#abcdef', ['#', ';']));
+  AssertEquals('Empty string', '', CopyUpTo('', ['#', ';']));
+end;
+
+procedure TTestMarkdownUtils.TestCopySkipped;
+begin
+  AssertEquals('Skip leading spaces', 'abc', CopySkipped('  abc', [' ']));
+  AssertEquals('Skip leading tabs', 'abc', CopySkipped(#9#9'abc', [#9]));
+  AssertEquals('Skip mixed leading whitespace', 'abc', CopySkipped(#9' abc', [' ', #9]));
+  AssertEquals('No chars to skip', 'abc', CopySkipped('abc', [' ']));
+  AssertEquals('String with only skippable chars', '', CopySkipped('  ', [' ']));
+  AssertEquals('Empty string', '', CopySkipped('', [' ']));
+end;
+
+procedure TTestMarkdownUtils.TestCopyMatching;
+begin
+  AssertEquals('Match leading digits', '123', CopyMatching('123abc', ['0'..'9']));
+  AssertEquals('No matching chars at start', '', CopyMatching('abc123', ['0'..'9']));
+  AssertEquals('String with only matching chars', '123', CopyMatching('123', ['0'..'9']));
+  AssertEquals('Empty string', '', CopyMatching('', ['0'..'9']));
+end;
+
+procedure TTestMarkdownUtils.TestStartsWithWS;
+var
+  Len: Integer;
+begin
+  AssertTrue('Char: one space', StartsWithWhitespace(' >', '>', Len));
+  AssertEquals('Char: Length for one space', 1, Len);
+
+  AssertTrue('Char: three spaces', StartsWithWhitespace('   >', '>', Len));
+  AssertEquals('Char: Length for three spaces', 3, Len);
+
+  AssertFalse('Char: four spaces (more than default wsLen=3)', StartsWithWhitespace('    >', '>', Len));
+
+  AssertTrue('Char: no space', StartsWithWhitespace('>', '>', Len));
+  AssertEquals('Char: Length for no space', 0, Len);
+
+  AssertFalse('Char: wrong prefix', StartsWithWhitespace('x>', '>', Len));
+
+  AssertTrue('String: one space', StartsWithWhitespace(' item', 'item', Len));
+  AssertEquals('String: Length for one space', 1, Len);
+
+  AssertTrue('String: three spaces', StartsWithWhitespace('   item', 'item', Len));
+  AssertEquals('String: Length for three spaces', 3, Len);
+  // Todo: take into account tabs
+  // AssertTrue('String: one tab', StartsWithWhitespace(#9'item', 'item', Len));
+  // AssertEquals('String: Length for one tab', 1, Len);
+end;
+
+procedure TTestMarkdownUtils.TestLeadingWhitespace;
+var
+  Tabs, Chars: Integer;
+begin
+  AssertEquals('Should be 0 for "abc"', 0, LeadingWhitespace('abc'));
+  AssertEquals('Should be 2 for "  abc"', 2, LeadingWhitespace('  abc'));
+  AssertEquals('Should be 4 for tab', 4, LeadingWhitespace(#9'abc'));
+  AssertEquals('Should be 4 for space-tab', 4, LeadingWhitespace(' '#9'abc'));
+  AssertEquals('Should be 4 for 2space-tab', 4, LeadingWhitespace('  '#9'abc'));
+  AssertEquals('Should be 4 for 3space-tab', 4, LeadingWhitespace('   '#9'abc'));
+  AssertEquals('Should be 8 for 4space-tab', 8, LeadingWhitespace('    '#9'abc'));
+  AssertEquals('Should be 8 for 2 tabs', 8, LeadingWhitespace(#9#9'abc'));
+
+  AssertEquals('Check returned spaces', 4, LeadingWhitespace(' '#9'abc', Tabs, Chars));
+  AssertEquals('Check returned tabs', 1, Tabs);
+  AssertEquals('Check returned whitespace chars', 2, Chars);
+end;
+
+procedure TTestMarkdownUtils.TestLengthWhiteSpaceCorrected;
+begin
+  AssertEquals('Length of "abc"', 3, lengthWhitespaceCorrected('abc'));
+  AssertEquals('Length of tab', 4, lengthWhiteSpaceCorrected(#9));
+  AssertEquals('Length of "a<tab>b"', 5, lengthWhiteSpaceCorrected('a'#9'b')); // 1 + 3 (tab at col 2) + 1
+  AssertEquals('Length of "abcd<tab>"', 8, lengthWhiteSpaceCorrected('abcd'#9)); // 4 + 4 (tab at col 5)
+end;
+
+procedure TTestMarkdownUtils.TestRemoveWS;
+begin
+  AssertEquals('Remove 2 spaces', 'abc', RemoveLeadingWhiteSpace('  abc', 2));
+  AssertEquals('Remove 2 of 4 spaces', '  abc', RemoveLeadingWhiteSpace('    abc', 2));
+  AssertEquals('Remove 1 tab (width 4)', 'abc', RemoveLeadingWhiteSpace(#9'abc', 4));
+  AssertEquals('Remove 2 spaces from tab (width 4)', '  abc', RemoveLeadingWhiteSpace(#9'abc', 2));
+  AssertEquals('Remove space and tab (total width 4)', 'abc', RemoveLeadingWhiteSpace(' '#9'abc', 4));
+end;
+
+procedure TTestMarkdownUtils.TestStripWhitespace;
+begin
+  AssertEquals('Strip from " a b c "', 'abc', stripWhitespace(' a b c '));
+  AssertEquals('Strip with tabs and newlines', 'abc', stripWhitespace(#9'a'#10'b c'));
+  AssertEquals('Strip from "abc"', 'abc', stripWhitespace('abc'));
+  AssertEquals('Strip only whitespace', '', stripWhitespace(' '#9#10));
+end;
+
+procedure TTestMarkdownUtils.TestHtmlEscape;
+begin
+  AssertEquals('Escape <', '&lt;', HtmlEscape('<'));
+  AssertEquals('Escape >', '&gt;', HtmlEscape('>'));
+  AssertEquals('Escape "', '&quot;', HtmlEscape('"'));
+  AssertEquals('Escape &', '&amp;', HtmlEscape('&'));
+  AssertEquals('Escape a', 'a', HtmlEscape('a'));
+  AssertEquals('Escape full string', '&lt;a href=&quot;url&quot;&gt; &amp; b', HtmlEscape('<a href="url"> & b'));
+end;
+
+procedure TTestMarkdownUtils.TestUrlEscape;
+begin
+  AssertEquals('URL Escape "a"', 'a', urlEscape('a'));
+  AssertEquals('URL Escape "&"', '&amp;', urlEscape('&')); // Ampersand should be HTML-escaped
+  AssertEquals('URL Escape "["', '%5B', urlEscape('[')); // Square bracket
+  AssertEquals('URL Escape "`"', '%60', urlEscape('`')); // Backtick
+  AssertEquals('URL Escape "é"', '%C3%A9', urlEscape('é')); // Unicode char é (UTF-8 bytes C3 A9)
+  AssertEquals('URL Escape "€"', '%E2%82%AC', urlEscape('€')); // Unicode char € (UTF-8 bytes E2 82 AC)
+  AssertEquals('URL Escape full string', 'a path with &amp; %E2%82%AC', urlEscape('a path with & €'));
+end;
+
+procedure TTestMarkdownUtils.TestParseEntityString;
+begin
+  AssertEquals('Parse &amp;', '&', parseEntityString(FEntities, '&amp;'));
+  AssertEquals('Parse &lt;', '<', parseEntityString(FEntities, '&lt;'));
+  AssertEquals('Parse unknown', '', parseEntityString(FEntities, '&unknown;'));
+  AssertEquals('Parse numeric &#60;', '<', parseEntityString(FEntities, '&#60;'));
+  AssertEquals('Parse numeric &#8364;', '€', parseEntityString(FEntities, '&#8364;'));
+  AssertEquals('Parse invalid numeric &#0;', #$FFFD, UTF8Decode(parseEntityString(FEntities, '&#0;')));
+end;
+
+procedure TTestMarkdownUtils.TestCheckForEntity;
+begin
+  AssertEquals('Check for &amp without ;', 5, CheckForTrailingEntity('&amp;'));
+  AssertEquals('Check for test&amp without ;', 5, CheckForTrailingEntity('test&amp;'));
+  AssertEquals('Check for &amp with ;', 5, CheckForTrailingEntity('&amp;')); // Semicolon is not alphanumeric, so it fails
+  AssertEquals('Check with space', 5, CheckForTrailingEntity('test &amp;')); // Space fails the check
+end;
+
+procedure TTestMarkdownUtils.TestIsRegexMatch;
+begin
+  AssertTrue('Substring match', isRegexMatch('content', 'ont'));
+  AssertTrue('Start anchor match', isRegexMatch('content', '^con'));
+  AssertFalse('Start anchor fail', isRegexMatch('content', '^ont'));
+  AssertFalse('Empty content never matches', isRegexMatch('', 'a'));
+end;
+
+procedure TTestMarkdownUtils.TestIsUnicodePunctuation;
+begin
+  AssertTrue('. should be punctuation', isUnicodePunctuation('.'));
+  AssertTrue('! should be punctuation', isUnicodePunctuation('!'));
+  AssertFalse('"a" should not be punctuation', isUnicodePunctuation('a'));
+  AssertFalse('"7" should not be punctuation', isUnicodePunctuation('7'));
+end;
+
+procedure TTestMarkdownUtils.TestCountStartChars;
+begin
+  AssertEquals('Count 3 chars', 3, CountStartChars('---abc', '-'));
+  AssertEquals('Count 0 chars', 0, CountStartChars('abc---', '-'));
+  AssertEquals('Count 4 chars', 4, CountStartChars('----', '-'));
+  AssertEquals('Count in empty string', 0, CountStartChars('', '-'));
+end;
+
+procedure TTestMarkdownUtils.TestToUnicodeChars;
+var
+  arr: TUnicodeCharDynArray;
+begin
+  arr := ToUnicodeChars('a€b');
+  AssertEquals('Array length for "a€b"', 3, Length(arr));
+  if Length(arr) = 3 then
+  begin
+    AssertEquals('First char should be a', 'a', arr[0]);
+    AssertEquals('Second char should be €','€', arr[1]);
+    AssertEquals('Third char should be b', 'b', arr[2]);
+  end;
+  AssertEquals('Array length for empty string', 0, Length(ToUnicodeChars('')));
+end;
+
+procedure TTestMarkdownUtils.TestTransformTabs;
+begin
+  AssertEquals('Transform leading tab', '    abc', TransformTabs(#9'abc'));
+  AssertEquals('Transform leading space and tab', '    abc', TransformTabs(' '#9'abc'));
+  AssertEquals('No change for no tabs', 'abc', TransformTabs('abc'));
+  AssertEquals('No change for tab in middle', 'abc'#9'def', TransformTabs('abc'#9'def'));
+end;
+
+initialization
+  RegisterTest(TTestMarkdownUtils);
+end.

+ 6 - 0
packages/fcl-md/tools/README.md

@@ -0,0 +1,6 @@
+This directory contains a tool to convert the list of HTML unicode entities at
+[https://html.spec.whatwg.org/entities.json](https://html.spec.whatwg.org/entities.json)
+to a list of name/unicode char mappings. 
+
+See the [markdown.htmlentities](../src/markdown.htmlentities.pas) file for
+the result.

+ 56 - 0
packages/fcl-md/tools/json2entities.lpi

@@ -0,0 +1,56 @@
+<?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="json2entities"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <Units>
+      <Unit>
+        <Filename Value="json2entities.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="json2entities"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions>
+      <Item>
+        <Name Value="EAbort"/>
+      </Item>
+      <Item>
+        <Name Value="ECodetoolError"/>
+      </Item>
+      <Item>
+        <Name Value="EFOpenError"/>
+      </Item>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 126 - 0
packages/fcl-md/tools/json2entities.lpr

@@ -0,0 +1,126 @@
+program json2entities;
+
+uses fpjson, jsonparser, sysutils, classes;
+
+function GetJSONData(const aFileName : string) : TJSONObject;
+var
+  lData : TJSONData;
+  lFile : TFileStream;
+begin
+  lFile:=TFileStream.Create(aFileName,fmOpenRead);
+  try
+    lData:=GetJSON(lFile);
+    if not (lData is TJSONObject) then
+      begin
+      lData.Free;
+      Raise EJSON.Create('Not a JSON object');
+      end;
+    Result:=TJSONObject(lData);
+  finally
+    lFile.Free;
+  end;
+end;
+
+Procedure GetUnitEnd(aUnit : TStrings);
+begin
+  With aUnit do
+    begin
+    Add('');
+    Add('implementation');
+    Add('');
+    Add('end.');
+    end;
+end;
+
+Procedure GetUnitStart(aUnit : TStrings; const aFileName : string);
+
+begin
+  With aUnit do
+    begin
+    Add('unit %s;',[ChangeFileExt(ExtractFileName(aFileName),'')]);
+    Add('');
+    Add('// see also https://html.spec.whatwg.org/entities.json');
+    Add('');
+    Add('interface');
+    Add('');
+    Add('Type');
+    Add('  THTMLEntityDef = record');
+    Add('    e : AnsiString;');
+    Add('    u : Unicodestring;');
+    Add('  end;');
+    Add('  THTMLEntityDefList = Array of THTMLEntityDef;');
+    Add('');
+    end;
+end;
+
+Procedure JSONToConst(lJSON : TJSONObject; aUnit : TStrings);
+
+var
+  I,L : integer;
+  ln,LastN,N,VS : String;
+  U : UnicodeString;
+  UC : UnicodeChar;
+  lList : TStringList;
+
+
+begin
+  lList:=TstringList.Create;
+  LastN:=N;
+  with aUnit do
+    begin
+    Add('const');
+    Add('  EntityDefList : THTMLEntityDefList = (');
+    For I:=0 to lJSON.Count-1 do
+      begin
+      N:=lJSON.Names[i];
+      System.Delete(N,1,1);
+      l:=Length(N);
+      if N[L]=';' then
+        SetLength(N,L-1);
+      if N=LastN then
+        continue;
+      LastN:=N;
+      U:=UTF8Decode(TJSONObject(lJSON.Items[i]).Get('characters',''));
+      VS:='';
+      For UC in U do
+        VS:=VS+'#$'+HexStr(Ord(UC),4);
+      ln:=Format('(e:''%s''; u: %s)',[N,VS]);
+      lList.Add(ln);
+      end;
+    for I:=0 to lList.Count-1 do
+      begin
+      ln:=lList[I];
+      if I<LList.Count-1 then
+        ln:=ln+',';
+      Add('    '+ln);
+      end;
+    Add(');');
+    end;
+
+end;
+
+var
+  lJSON : TJSONObject;
+  lUnit : TStrings;
+
+begin
+  if ParamCount<2 then
+    begin
+    Writeln('Usage: ',ParamStr(0),' inputfile outputfile');
+    Halt(1);
+    end;
+  lUnit:=Nil;
+  lJSON:=GetJSONData(ParamStr(1));
+  try
+    lUnit:=TStringList.Create;
+    GetUnitStart(LUnit,ParamStr(2));
+    JSONToConst(lJSON,lUnit);
+    GetUnitEnd(lUnit);
+    lUnit.SaveToFile(ParamStr(2));
+  finally
+    lJSON.Free;
+    lUnit.Free;
+  end;
+
+end.
+

+ 11 - 10
packages/fpmake_add.inc

@@ -33,6 +33,17 @@
   add_fcl_stl(ADirectory+IncludeTrailingPathDelimiter('fcl-stl'));
   add_fcl_stl(ADirectory+IncludeTrailingPathDelimiter('fcl-stl'));
   add_fcl_web(ADirectory+IncludeTrailingPathDelimiter('fcl-web'));
   add_fcl_web(ADirectory+IncludeTrailingPathDelimiter('fcl-web'));
   add_fcl_xml(ADirectory+IncludeTrailingPathDelimiter('fcl-xml'));
   add_fcl_xml(ADirectory+IncludeTrailingPathDelimiter('fcl-xml'));
+  add_fcl_pdf(ADirectory+IncludeTrailingPathDelimiter('fcl-pdf'));
+  add_fcl_md(ADirectory+IncludeTrailingPathDelimiter('fcl-md'));
+  add_fcl_css(ADirectory+IncludeTrailingPathDelimiter('fcl-css'));
+  add_fcl_jsonschema(ADirectory+IncludeTrailingPathDelimiter('fcl-jsonschema'));
+  add_fcl_openapi(ADirectory+IncludeTrailingPathDelimiter('fcl-openapi'));
+  add_fcl_wit(ADirectory+IncludeTrailingPathDelimiter('fcl-wit'));
+  add_fcl_ebnf(ADirectory+IncludeTrailingPathDelimiter('fcl-ebnf'));
+  add_fcl_yaml(ADirectory+IncludeTrailingPathDelimiter('fcl-yaml'));
+  add_fcl_fpterm(ADirectory+IncludeTrailingPathDelimiter('fcl-fpterm'));
+  add_fcl_report(ADirectory+IncludeTrailingPathDelimiter('fcl-report'));
+  add_fcl_mustache(ADirectory+IncludeTrailingPathDelimiter('fcl-mustache'));
   add_fftw(ADirectory+IncludeTrailingPathDelimiter('fftw'));
   add_fftw(ADirectory+IncludeTrailingPathDelimiter('fftw'));
   add_fpgtk(ADirectory+IncludeTrailingPathDelimiter('fpgtk'));
   add_fpgtk(ADirectory+IncludeTrailingPathDelimiter('fpgtk'));
   add_fpindexer(ADirectory+IncludeTrailingPathDelimiter('fpindexer'));
   add_fpindexer(ADirectory+IncludeTrailingPathDelimiter('fpindexer'));
@@ -138,35 +149,25 @@
   add_libpipewire(ADirectory+IncludeTrailingPathDelimiter('libpipewire'));
   add_libpipewire(ADirectory+IncludeTrailingPathDelimiter('libpipewire'));
   add_zorba(ADirectory+IncludeTrailingPathDelimiter('zorba'));
   add_zorba(ADirectory+IncludeTrailingPathDelimiter('zorba'));
   add_Google(ADirectory+IncludeTrailingPathDelimiter('googleapi'));
   add_Google(ADirectory+IncludeTrailingPathDelimiter('googleapi'));
-  add_fcl_pdf(ADirectory+IncludeTrailingPathDelimiter('fcl-pdf'));
   add_odata(ADirectory+IncludeTrailingPathDelimiter('odata'));
   add_odata(ADirectory+IncludeTrailingPathDelimiter('odata'));
   add_pastojs(ADirectory+IncludeTrailingPathDelimiter('pastojs'));
   add_pastojs(ADirectory+IncludeTrailingPathDelimiter('pastojs'));
   add_libgc(ADirectory+IncludeTrailingPathDelimiter('libgc'));
   add_libgc(ADirectory+IncludeTrailingPathDelimiter('libgc'));
   add_libfontconfig(ADirectory+IncludeTrailingPathDelimiter('libfontconfig'));
   add_libfontconfig(ADirectory+IncludeTrailingPathDelimiter('libfontconfig'));
-  add_fcl_report(ADirectory+IncludeTrailingPathDelimiter('fcl-report'));
   add_webidl(ADirectory+IncludeTrailingPathDelimiter('webidl'));
   add_webidl(ADirectory+IncludeTrailingPathDelimiter('webidl'));
   add_gnutls(ADirectory+IncludeTrailingPathDelimiter('gnutls'));
   add_gnutls(ADirectory+IncludeTrailingPathDelimiter('gnutls'));
   add_ide(ADirectory+IncludeTrailingPathDelimiter('ide'));
   add_ide(ADirectory+IncludeTrailingPathDelimiter('ide'));
   add_libpcre(ADirectory+IncludeTrailingPathDelimiter('libpcre'));
   add_libpcre(ADirectory+IncludeTrailingPathDelimiter('libpcre'));
   add_vclcompat(ADirectory+IncludeTrailingPathDelimiter('vcl-compat'));
   add_vclcompat(ADirectory+IncludeTrailingPathDelimiter('vcl-compat'));
   add_qlunits(ADirectory+IncludeTrailingPathDelimiter('qlunits'));
   add_qlunits(ADirectory+IncludeTrailingPathDelimiter('qlunits'));
-  add_mustache(ADirectory+IncludeTrailingPathDelimiter('fcl-mustache'));
   add_wasmtime(ADirectory+IncludeTrailingPathDelimiter('wasmtime'));
   add_wasmtime(ADirectory+IncludeTrailingPathDelimiter('wasmtime'));
   add_wasmedge(ADirectory+IncludeTrailingPathDelimiter('wasmedge'));
   add_wasmedge(ADirectory+IncludeTrailingPathDelimiter('wasmedge'));
   add_gitlab(ADirectory+IncludeTrailingPathDelimiter('gitlab'));
   add_gitlab(ADirectory+IncludeTrailingPathDelimiter('gitlab'));
-  add_fcl_css(ADirectory+IncludeTrailingPathDelimiter('fcl-css'));
   add_gstreamer(ADirectory+IncludeTrailingPathDelimiter('gstreamer'));
   add_gstreamer(ADirectory+IncludeTrailingPathDelimiter('gstreamer'));
   add_testinsight(ADirectory+IncludeTrailingPathDelimiter('testinsight'));
   add_testinsight(ADirectory+IncludeTrailingPathDelimiter('testinsight'));
   add_wasm_job(ADirectory+IncludeTrailingPathDelimiter('wasm-job'));
   add_wasm_job(ADirectory+IncludeTrailingPathDelimiter('wasm-job'));
   add_wasm_utils(ADirectory+IncludeTrailingPathDelimiter('wasm-utils'));
   add_wasm_utils(ADirectory+IncludeTrailingPathDelimiter('wasm-utils'));
   add_wasm_oi(ADirectory+IncludeTrailingPathDelimiter('wasm-oi'));
   add_wasm_oi(ADirectory+IncludeTrailingPathDelimiter('wasm-oi'));
-  add_fcl_jsonschema(ADirectory+IncludeTrailingPathDelimiter('fcl-jsonschema'));
-  add_fcl_openapi(ADirectory+IncludeTrailingPathDelimiter('fcl-openapi'));
-  add_fcl_wit(ADirectory+IncludeTrailingPathDelimiter('fcl-wit'));
-  add_fcl_ebnf(ADirectory+IncludeTrailingPathDelimiter('fcl-ebnf'));
-  add_fcl_yaml(ADirectory+IncludeTrailingPathDelimiter('fcl-yaml'));
   add_ptckvm(ADirectory+IncludeTrailingPathDelimiter('ptckvm'));
   add_ptckvm(ADirectory+IncludeTrailingPathDelimiter('ptckvm'));
-  add_fcl_fpterm(ADirectory+IncludeTrailingPathDelimiter('fcl-fpterm'));
   add_libjack(ADirectory+IncludeTrailingPathDelimiter('libjack'));
   add_libjack(ADirectory+IncludeTrailingPathDelimiter('libjack'));
   add_libsndfile(ADirectory+IncludeTrailingPathDelimiter('libsndfile'));
   add_libsndfile(ADirectory+IncludeTrailingPathDelimiter('libsndfile'));
   add_redis(ADirectory+IncludeTrailingPathDelimiter('redis'));
   add_redis(ADirectory+IncludeTrailingPathDelimiter('redis'));

+ 7 - 1
packages/fpmake_proc.inc

@@ -144,6 +144,12 @@ begin
 {$include fcl-json/fpmake.pp}
 {$include fcl-json/fpmake.pp}
 end;
 end;
 
 
+procedure add_fcl_md(const ADirectory: string);
+begin
+  with Installer do
+{$include fcl-md/fpmake.pp}
+end;
+
 procedure add_fcl_net(const ADirectory: string);
 procedure add_fcl_net(const ADirectory: string);
 begin
 begin
   with Installer do
   with Installer do
@@ -858,7 +864,7 @@ begin
 {$include qlunits/fpmake.pp}
 {$include qlunits/fpmake.pp}
 end;
 end;
 
 
-procedure add_mustache(const ADirectory: string);
+procedure add_fcl_mustache(const ADirectory: string);
 begin
 begin
   with Installer do
   with Installer do
 {$include fcl-mustache/fpmake.pp}
 {$include fcl-mustache/fpmake.pp}