2008. augusztus 16., szombat

Syntax Highlighted Source Code Export to HTML or RTF


Problem/Question/Abstract:

It was asked for an easy way to export all types of source code to HTML.  The Open Source SynEdit components provide this functionality.  Using those I created a simple utility to allow for command line driven exporting of most source code to both HTML and RTF formats.

Answer:

{
Syntax Highlighted Source Code Export to HTML or RTF
Written and (c) 2002 by Jim McKeeth jim@bsdg.org

Pascal, Borland Dfm, HTML, CSS, HC11, ADSP21xx, AWK, Baan, Cache,
CAC, CPM, Fortran, Foxpro, Galaxy, Dml, General, GWScript, HP48, INI, Inno, Java, JScript, Kix, Modelica, M3, VBScript, Bat, Perl, PHP, Progress, SDD, SQL, SML, TclTk, VB, Asm, Cpp, Python to HTML or RTF with end user customization.
\Uses the open source SynEdit component suite.

It was asked for an easy way to export all types of source code to HTML.  The Open Source SynEdit components provide this functionality.  Using those I created a simple utility to allow for command line driven exporting of most source code to both HTML and RTF formats.

Note, this was written in Delphi 6 but should work with C++ Builder 3 or better, Delphi 3 or better or Kylix with only minimal changes.

First rule when developing with Delphi: No need to reinvent the wheel.  Sure, I could have come up with my own routines to format source code to HTML, but why when SynEdit is freely available and works great.  Before you start, you will need to download and install the SynEdit suite of components from
http://synedit.sourceforge.net/ .

There are three main parts to this: Parse the command-line parameters;  Verify the parameters;  Format the source code.

Here is the unit header along with a list of internal supported highlighters.
}
unit ExportUnit;

{ Internal supported highlighter keywords
Pas
Dfm
HTML
Css
HC11
ADSP21xx
AWK
Baan
Cache
CAC
CPM
Fortran
Foxpro
Galaxy
Dml
General
GWScript
HP48
Ini
Inno
Java
JScript
Kix
Modelica
M3
VBScript
Bat
Perl
PHP
Progress
SDD
SQL
SML
TclTk
VB
Asm
Cpp
Python
}

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Dialogs, Controls, Forms,
    SynHighlighterAsm, SynHighlighterVB, SynHighlighterTclTk, SynHighlighterSml,
    SynHighlighterSQL, SynHighlighterSDD, SynHighlighterPython, SynHighlighterProgress,
    SynHighlighterPHP, SynHighlighterPerl, SynHighlighterBat, SynHighlighterVBScript,
    SynHighlighterM3, SynHighlighterModelica, SynHighlighterKix, SynHighlighterJScript,
    SynHighlighterJava, SynHighlighterInno, SynHighlighterIni, SynHighlighterHtml,
    SynHighlighterHP48, SynHighlighterGWS, SynHighlighterGeneral, SynHighlighterDml,
    SynHighlighterGalaxy, SynHighlighterFoxpro, SynHighlighterFortran,
    SynHighlighterDfm, SynHighlighterCPM, SynHighlighterCss, SynHighlighterCAC,
    SynHighlighterCache, SynHighlighterCpp, SynHighlighterBaan, SynHighlighterAWK,
    SynHighlighterADSP21xx, SynHighlighterHC11, SynEditHighlighter, SynHighlighterPas,
    SynExportRTF, SynEditExport, SynExportHTML, SynHighlighterMulti, StdCtrls, ExtCtrls;

{
First we need to setup the form.  I simple have a large TMemo called memoLog that is set to client justified.  Now we add the SynEdit components we need.
Simply add one TsynExporterHTML and one TsynExporterRTF from the SynEdit tab.
Rename them ExporterHTML and ExporterRTF respeively.  Now add the
TsynHighlightManager.  When you add this component it brings up a dialog allowing you to choose which Highlighters to add.  Simple click "Select All" and "Ok" to add one of each.  Leave the names as the defaults.
}

type
  TformSynEdit = class(TForm)
    ExporterHTML: TSynExporterHTML;
    ExporterRTF: TSynExporterRTF;
    memoLog: TMemo;
    SynHC11Syn1: TSynHC11Syn;
    SynADSP21xxSyn1: TSynADSP21xxSyn;
    SynAWKSyn1: TSynAWKSyn;
    SynBaanSyn1: TSynBaanSyn;
    SynCppSyn1: TSynCppSyn;
    SynCacheSyn1: TSynCacheSyn;
    SynCACSyn1: TSynCACSyn;
    SynCssSyn1: TSynCssSyn;
    SynCPMSyn1: TSynCPMSyn;
    SynDfmSyn1: TSynDfmSyn;
    SynFortranSyn1: TSynFortranSyn;
    SynFoxproSyn1: TSynFoxproSyn;
    SynGalaxySyn1: TSynGalaxySyn;
    SynDmlSyn1: TSynDmlSyn;
    SynGeneralSyn1: TSynGeneralSyn;
    SynGWScriptSyn1: TSynGWScriptSyn;
    SynHP48Syn1: TSynHP48Syn;
    SynHTMLSyn1: TSynHTMLSyn;
    SynIniSyn1: TSynIniSyn;
    SynInnoSyn1: TSynInnoSyn;
    SynJavaSyn1: TSynJavaSyn;
    SynJScriptSyn1: TSynJScriptSyn;
    SynKixSyn1: TSynKixSyn;
    SynModelicaSyn1: TSynModelicaSyn;
    SynM3Syn1: TSynM3Syn;
    SynVBScriptSyn1: TSynVBScriptSyn;
    SynBatSyn1: TSynBatSyn;
    SynPasSyn1: TSynPasSyn;
    SynPerlSyn1: TSynPerlSyn;
    SynPHPSyn1: TSynPHPSyn;
    SynProgressSyn1: TSynProgressSyn;
    SynPythonSyn1: TSynPythonSyn;
    SynSDDSyn1: TSynSDDSyn;
    SynSQLSyn1: TSynSQLSyn;
    SynSMLSyn1: TSynSMLSyn;
    SynTclTkSyn1: TSynTclTkSyn;
    SynVBSyn1: TSynVBSyn;
    SynAsmSyn1: TSynAsmSyn;
    procedure PerformHighlight(const sInFile, sOutFile: string;
      sceHighlighter: TSynCustomExporter);
    procedure VerifyParameters(const sInFile: string; sOutFile: string = '';
      sHighlighter: string = '');
    procedure ParseParameters;
    procedure FormShow(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    procedure Log(s: string);

  end;

var
  formSynEdit: TformSynEdit;

  {These are some simple string parsing rontines that wrote a long time ago and
  use in many of my new programs.}
function pright(const s, divisor: string): string;
function pleft(const s, divisor: string): string;
function ReverseStr(const s: string): string;
function ValueOf(const S: string): string;
function NameOf(const S: string): string;

implementation

{$R *.dfm}

{ I have a method for adding lines to he memo called log.
I use the ~ or character #126 for line breaks.}

procedure TformSynEdit.Log(s: string);
begin
  memoLog.Lines.Add(StringReplace(s, #126, #13#10, [rfReplaceAll]));
end;

{Returns the portion of the string left of the divisor}

function pleft(const s, divisor: string): string;
begin
  if pos(divisor, s) > 0 then
    result := copy(s, 1, pos(divisor, s) - 1)
  else
    result := s;
end;

{Returns string in reverse}

function ReverseStr(const s: string): string;
var
  ctr: Integer;
  s2: string;
begin
  s2 := s;
  for ctr := 1 to length(s) do
    s2[length(s) - ctr + 1] := s[ctr];
  result := s2;
end;

{Returns the portion of the string right of the divisor}

function pright(const s, divisor: string): string;
var
  rs: string;
begin
  rs := ReverseStr(s);
  result := ReverseStr(PLeft(rs, reverseStr(divisor)));
end;

{Returns the portion of the string right of the '='}

function ValueOf(const s: string): string;
begin
  result := pright(s, pleft(s, '=') + '=');
end;

{Returns the portion of the string left of the '='}

function NameOf(const s: string): string;
begin
  result := pleft(s, '=');
end;

{This is called from the VerifyParamters routine to find a matching highlighter based on the extension of the input file.  It works by seperating each extension of the filter as a item in a string list and then look to see if the specified extension is in the list.}

function FilterMatch(sExt, sFilter: string): Boolean;
var
  slExts: TStringList;
begin
  slExts := TStringList.Create;
  try
    slExts.Delimiter := ';';
    slExts.DelimitedText := pright(sFilter, '|');
    Result := slExts.indexof('*' + sExt) > -1;
  finally
    slExts.Free;
  end;
end;

{This routine is not currently used, but was used to save some template highlighters to disk.}
{
function ComponentToFile(Component: TComponent; const sFileName: string)
           : boolean;
var
  BinStream: TMemoryStream;
  FileStream: TFileStream;
begin
  BinStream := TMemoryStream.Create;
  try
    FileStream := TFileStream.Create(sFileName, fmCreate or fmShareExclusive);
    try
      BinStream.WriteComponent(Component);  // write the component to the stream
      BinStream.Seek(0, soFromBeginning);  // seek to the origin of the stream
      // convert the binary representation of the component to easily editable
      // text format and save it to a FileStream
      ObjectBinaryToText(BinStream, FileStream);
      Result:= True;
    finally
      FileStream.Free;
    end;
  finally
    BinStream.Free
  end;
end;
}

{This is the routine used to load the external highlighter as a component.}

function FileToComponent(sFileName: string): TComponent;
var
  FileStream: TFileStream;
  BinStream: TMemoryStream;
begin
  FileStream := TFileStream.Create(sFileName, fmOpenRead or fmShareExclusive);
  try
    BinStream := TMemoryStream.Create;
    try
      // convert the user editable text format to binary
      ObjectTextToBinary(FileStream, BinStream);
      BinStream.Seek(0, soFromBeginning); // seek to the origin of the stream
      // create the component from the stream
      Result := BinStream.ReadComponent(nil);
    finally
      BinStream.Free;
    end;
  finally
    FileStream.Free;
  end;
end;

{-- Parsing command-line parameters --

We accept up to three parameters, but only require one.
Here is the usage statement:

Command-line usage:
> SrcFormat IN=(Input File) [OUT=(Output File)] [HIGHLIGHTER=(Highligher Name)]

Where
> IN=(Input File) is the required input file.
  Example: 'In="C:\My Documents\Source\SrcFormat.dpr"'
> OUT=(Output File) is the optional output file.
  Format is based on extension HTML or RTF.
  Default is same file name and path with an additional '.HTM'.
  Example: 'Out=C:\Source.RTF'
> HIGHLIGHTER=(Highlighter Name) is the optional Highlighter to use.
  If not provided then guessed based on extension.
  Can also be the file name of a saved Highlighter.
  Example: 'Highlighter=Pas'
  Example: 'Highlighter="C:\My Documents\Highlighters\MyPascal.hi"'

We only require that they specify the input file, and in fact if they only
specify a single parameter, even without the "IN=" prefix, then we assume it is
the input file.  We attempt an educated guess on the rest.

Here is the code we can use to parse the command-line parameters:}

procedure TformSynEdit.ParseParameters;
var
  sInFile, sOutFile, sHighlighter: string;
  iCtr: Integer;
begin
  if (ParamCount = 1) and (FileExists(ParamStr(1))) then
    sInFile := ParamStr(1) // if only one then it is the input file
  else if ParamCount > 0 then
    for iCtr := 1 to ParamCount do // spin though the parameters
    begin
      if CompareText(NameOf(ParamStr(iCtr)), 'IN') = 0 then
        sInFile := ValueOf(ParamStr(iCtr)) // Input file
      else if CompareText(NameOf(ParamStr(iCtr)), 'OUT') = 0 then
        sOutFile := ValueOf(ParamStr(iCtr)) // Output file
      else if CompareText(NameOf(ParamStr(iCtr)), 'HIGHLIGHTER') = 0 then
        sHighlighter := ValueOf(ParamStr(iCtr)) // highlighter
    end
  else
  begin // explain the usage
    Log('Command-line usage: '#126 +
      '> SrcFormat IN=(Input File) [OUT=(Output File)] ' +
      '[HIGHLIGHTER=(Highligher Name)]' + #126 + #126 +
      'Where' + #126 +
      '> IN=(Input File) is the required input file.  ' + #126 +
      '  Example: ''In="C:\My Documents\Source\SrcFormat.dpr"''' + #126 +
      '> OUT=(Output File) is the optional output file.  ' + #126 +
      '  Format is based on extension HTML or RTF.' + #126 +
      '  Default is same file name and path with an additional ''.HTM''.'
      + #126 +
      '  Example: ''Out=C:\Source.RTF''' + #126 +
      '> HIGHLIGHTER=(Highlighter Name) is the Highlighter to use.'
      + #126 +
      '  If not provided then guessed based on extension.  ' + #126 +
      '  Can also be the file name of a saved Highlighter.' + #126 +
      '  Example: ''Highlighter=Pas''' + #126 +
      '  Example: ''Highlighter="C:\My Documents\SrcExport\MyPascal.hi"'''
      );
    Exit;
  end;

  // Finally we pass all the variables to the VerifyParameters routine.
  VerifyParameters(sInFile, sOutFile, sHighlighter);
end;

{You could actually call this routine from a GUI interface as well as the
ParseParameters method, but I will let you add that functionality.  I'll step
you through each section of this routine.}

procedure TformSynEdit.VerifyParameters(const sInFile: string; sOutFile,
  sHighlighter: string);
var
  sInExt, sOutExt: string;
  myExporter: TSynCustomExporter;
  iCtr: Integer;
begin
  {First verify that the input file does exist.  We cannot format something that
  has not been saved to disk yet (although that would make a great Delphi Expert!)
  We simply add a log line and exit if the file is non-existent.}
  if not FileExists(sInFile) then
  begin
    Log('The input file "' + sInFile + '" does not exist');
    Exit;
  end;

  {If they did not specify an output file then we append an 'HTM' extension.}
  if sOutFile = '' then
    sOutFile := sInFile + '.HTM';

  {Make sure the output file does not exist.}
  if FileExists(sOutFile) then
  try
    DeleteFile(sOutFile);
  except
    log('Output file exists and cannot be deleted');
    Exit;
  end;

  {Make sure we can create the output file.}
  try
    // Make sure we can create the path
    ForceDirectories(ExtractFilePath(sOutFile));
    // Create and close a test file
    FileClose(FileCreate(sOutFile));
  except
    log('Cannot create output file!');
    Exit;
  end;

  {Extract the extensions of the files for guessing the highlighter and format.}
  sInExt := UpperCase(ExtractFileExt(sInFile));
  sOutExt := UpperCase(ExtractFileExt(sOutFile));

  {Now we guess the export format.
  If it is not an .RTF extension then we assume HTML.}
  if sOutExt = '.RTF' then
  begin
    log('Exporting to RTF');
    myExporter := ExporterRTF;
  end
  else
  begin
    log('Exporting to HTML');
    myExporter := ExporterHTML;
  end;

  {Now we guess the highlighter.  To do this we will spin through all the
  DefaultFilter properties of the highlighters we included on the form.  Since we
  stop on the first match, you may want to change the creation order (by right
  clicking on the form) to put your most common highlighters first.  }
  myExporter.Highlighter := nil;

  // only do with if no highlighter was specified at the command-line
  if sHighlighter = '' then
  begin
    for iCtr := 0 to pred(ComponentCount) do // go through all the componets
      // only look at highlighters
      if Components[iCtr] is TSynCustomHighlighter then
        // use the filter match method to see if the extension matches the filter
        if FilterMatch(sInExt,
          (Components[iCtr] as TSynCustomHighlighter).DefaultFilter) then
        begin
          // Set the name of the highlighter as the meaningful part of the
          // component name.
          sHighlighter := Copy(Components[iCtr].Name, 4,
            Length(Components[iCtr].Name) - 7);
          // set the actual highlighter property of the exporter
          myExporter.Highlighter := Components[iCtr] as TSynCustomHighlighter;
          // no more looping, we have what we want.
          Break;
        end;
  end;

  if sHighlighter = '' then
  begin // we didn't find an internal one, but we might find an external one.
    log('No highlighter was found for the extension ' + sInExt);
  end;

  // if they specified a highlighter at the command line find it now.
  if (myExporter.Highlighter = nil) and (sHighlighter <> '') then
    for iCtr := 0 to pred(ComponentCount) do
      if Components[iCtr] is TSynCustomHighlighter then
        if CompareText(Components[iCtr].Name, 'Syn' + sHighlighter + 'Syn1') = 0 then
        begin
          myExporter.Highlighter := Components[iCtr] as TSynCustomHighlighter;
          Break;
        end;

  // we still don't have a highlighter but one was specified
  if (myExporter.Highlighter = nil) and (sHighlighter <> '') then
  begin
    log('No internal highlighter named ''' + sHighlighter + ''' found!');
    // but there is a file with the same name as the specified highlighter,
    // maybe it is an external highlighter!
    if FileExists(sHighlighter) then
    begin
      log('Loading highlighter: ' + sHighlighter);
      // before you can load a component you need to register the class.
      RegisterClasses([TSynExporterHTML, TSynExporterRTF, TSynPasSyn,
        TSynDfmSyn, TSynHTMLSyn, TSynCssSyn, TSynHC11Syn, TSynADSP21xxSyn,
          TSynAWKSyn, TSynBaanSyn, TSynCacheSyn, TSynCACSyn, TSynCPMSyn,
          TSynFortranSyn, TSynFoxproSyn, TSynGalaxySyn, TSynDmlSyn,
          TSynGeneralSyn, TSynGWScriptSyn, TSynHP48Syn, TSynIniSyn, TSynInnoSyn,
          TSynJavaSyn, TSynJScriptSyn, TSynKixSyn, TSynModelicaSyn, TSynM3Syn,
          TSynVBScriptSyn, TSynBatSyn, TSynPerlSyn, TSynPHPSyn, TSynProgressSyn,
          TSynSDDSyn, TSynSQLSyn, TSynSMLSyn, TSynTclTkSyn, TSynVBSyn,
          TSynAsmSyn, TSynCppSyn, TSynPythonSyn, TSynPasSyn]);
      try // try to load the component
        myExporter.Highlighter :=
          FileToComponent(sHighlighter) as TSynCustomHighlighter;
      except
        // failed to load the component, it must have been invalid!
        log('External highlighter named ''' + sHighlighter + ''' is inavlid!');
        Exit;
      end;
    end
    else
    begin
      log('No external highlighter named ''' + sHighlighter + ''' found!');
      Exit;
    end;
  end;

  // Note: if not highlighter was specifed, and none can be found based on
  // extension then we can export without a highlighter which results in
  // no syntax highlighting, but does change the format.

  // nothing caused us to exit along the way, we must have everything we need.
  // list it out in the log window
  Log('Intput file: ' + sInFile + #126 +
    'Output file: ' + sOutFile + #126 +
    'Highlighter: ' + sHighlighter);

  // Now we call Perform Highlight with the final parameters.
  PerformHighlight(sInFile, sOutFile, myExporter);
end;

{ After all that work, all we did is protect the actual functionality of
this program from the user.  We should now have valid parameters for this
method. }

procedure TformSynEdit.PerformHighlight(const sInFile, sOutFile: string;
  sceHighlighter: TSynCustomExporter);
var
  slSrc: TStringList;
begin
  slSrc := TStringList.Create; // to load the source code into
  try
    // load the source code from disk
    slSrc.LoadFromFile(sInFile);
    sceHighlighter.ExportAsText := True;
    // Might be a good idea to make this user definable at some point, but for
    // now we will just us a generic title
    sceHighlighter.Title := ExtractFileName(sInFile) + ' source code';
    // Read in the source code and convert it to the highlighter format
    sceHighlighter.ExportAll(slSrc);
    // Save the output to disk.
    sceHighlighter.SaveToFile(sOutFile);
  finally
    slSrc.Free; // all done
  end;
end;

{Now assign an event handler to the Form Show event.  You could use a button
instead if you wanted.  You might also want to close when it is done.}

procedure TformSynEdit.FormShow(Sender: TObject);
begin
  Application.ProcessMessages; // finish drawing the form
  ParseParameters; // Get Started.
end;

end.

Nincsenek megjegyzések:

Megjegyzés küldése