{ Converter for wiki pages to a chm file

  Copyright (C) 2012  Mattias Gaertner  mattias@freepascal.org

  This source is free software; you can redistribute it and/or modify it under
  the terms of the GNU General Public License as published by the Free
  Software Foundation; either version 2 of the License, or (at your option)
  any later version.

  This code 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.  See the GNU General Public License for more
  details.

  A copy of the GNU General Public License is available on the World Wide Web
  at <http://www.gnu.org/copyleft/gpl.html>. You can also obtain it by writing
  to the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
  Boston, MA 02110-1335, USA.


ToDo:
  Full text search
  images

}
unit Wiki2CHMConvert;

{$mode objfpc}{$H+}

{ $DEFINE VerboseCHMIndex}
{ $DEFINE EnableWikiCHMWriter}

interface

uses
  Classes, SysUtils,
  {$IFDEF EnableWikiCHMWriter}
  wikichmwriter, wikichmfilewriter, wikichmsitemap,
  {$ELSE}
  chmwriter, chmfilewriter, chmsitemap,
  {$ENDIF}
  // LazUtils
  LazLoggerBase, LazUTF8, LazFileUtils, AvgLvlTree,
  // LazWiki
  Wiki2HTMLConvert, Wiki2XHTMLConvert;

const
  CHMImagesDir = '/images/';
type

  { TW2CHMPage
    for future extensions and descendants }

  TW2CHMPage = class(TW2HTMLPage)
  public
  end;

  { TWiki2CHMConverter }

  TWiki2CHMConverter = class(TWiki2HTMLConverter)
  private
    FCHMFile: string;
    FTOCRootName: String;
    FIndexFileName, FTOCFileName: String;
    FTocSitemap, FIndexSitemap: TCHMSiteMap;
    FTocStream, FIndexStream: TMemoryStream;
    procedure SetCHMFile(AValue: string);
    procedure SetIndexFileName(AValue: string);
    procedure SetTOCFilename(AValue: string);
  protected
    Writer: TChmWriter;
    FilesCompressed: integer;
    DocumentNameToPage: TStringToPointerTree; // Page.WikiDocumentName+'.html' to Page
    procedure AddIndexItem(AText, AUrl: String); override;
    procedure AddTocItem(ALevel: Integer; AText, AUrl: String); override;
    procedure ConvertInit; override;
    function OnWriterGetFileData(const DataName: String; out PathInChm: String;
      out FileName: String; var Stream: TStream): Boolean;
    procedure OnWriterLastFileAdded(Sender: TObject);
    function GetImageLink(ImgFilename: string): string; override;
    function GetInternalImageLink(ImgFilename: String): String; override;
    function GetPageLink(Page: TW2XHTMLPage): string; override;
    procedure SaveAllPages; override;
  public
    constructor Create; override;
    destructor Destroy; override;
    procedure Clear; override;
    function GetRelativeCSSFileName: String; override;
    property CHMFile: string read FCHMFile write SetCHMFile;
    property IndexFileName: string read FIndexFileName write SetIndexFileName;
    property TOCFileName: String read FTOCFilename write SetTOCFilename;
    property TOCRootName: String read FTOCRootName write FTOCRootName;
  end;

implementation

{ TWiki2HTMLConverter }

function TWiki2CHMConverter.OnWriterGetFileData(const DataName: String; out
  PathInChm: String; out FileName: String; var Stream: TStream): Boolean;
var
  Page: TW2CHMPage;
  ImgFilename: String;
begin
  Stream.Size:=0;
  inc(FilesCompressed);
  debugln(['chm processing ',FilesCompressed,' of ',Writer.FilesToCompress.Count,' "',DataName,'" ...']);
  Result := False; // Return true to abort compressing files
  Filename:=ExtractFileName(DataName);
  PathInChm:='/'+ExtractFilePath(DataName);
  // cleanup string
  PathInChm:=StringReplace(PathInChm, '\','/',[rfReplaceAll]);
  PathInChm:=StringReplace(PathInChm, '//','/',[rfReplaceAll]);
  Page:=TW2CHMPage(DocumentNameToPage[DataName]);
  if Page<>nil then begin
    // a page
    SavePageToStream(Page,Stream);
  end else if copy(DataName,1,length(CHMImagesDir))=CHMImagesDir then begin
    // an image
    ImgFilename:=ImagesDir+copy(DataName,length(CHMImagesDir)+1,length(DataName));
    //debugln(['TWiki2CHMConverter.OnWriterGetFileData img="',DataName,'" File="',ImgFilename,'" FileSize=',FileSize(ImgFilename)]);
    TMemoryStream(Stream).LoadFromFile(ImgFilename);
  end else if DataName=GetRelativeCSSFilename then begin
    // the css file
    TMemoryStream(Stream).LoadFromFile(CSSFilename);
  end else
   raise Exception.Create('TWiki2CHMConverter.OnWriterGetFileData failed DataName="'+dbgstr(DataName)+'"');
end;

procedure TWiki2CHMConverter.AddIndexItem(AText, AUrl: String);
var
  {$IF FPC_FULLVERSION>=30200}
  x: integer;
  {$ENDIF}
  AItem: TCHMSiteMapItem;
  i: Integer;
  txt, url, itemtxt, itemurl, itemlocal: String;
begin
  // Avoid empty index data
  if (AText = '') or (AUrl = '') then
    exit;

  AText := EscapeToHTML(AText);

  // Avoid duplicate index items.
  txt := UTF8Trim(UTF8Lowercase(AText));
  url := UTF8Trim(UTF8Lowercase(AUrl));
  for i:=0 to FIndexSiteMap.Items.Count-1 do begin
    AItem := FIndexSiteMap.Items.Item[i];
    itemtxt := UTF8Lowercase(AItem.Text);
    {$IF FPC_FULLVERSION>=30200}
    URL:='';
    for x:=0 to AItem.SubItemcount-1 do
    begin
      URL:=AItem.SubItem[x].URL;
      if URL<>'' then
        break;
      URL:=AItem.SubItem[x].Local;
      if URL<>'' then
        break;
    end;
    {$ELSE}
    URL:=AItem.URL;
    {$ENDIF}
    itemurl := UTF8Lowercase(URL);
    itemlocal := UTF8Lowercase(AItem.Local);
    if (txt = itemtxt) and ((url = itemurl) or (url = itemlocal)) then
      exit;
  end;

  AItem := FIndexSiteMap.Items.NewItem;
  AItem.Text := UTF8Trim(AText);
  {$IF FPC_FULLVERSION>=30200}
  AItem.AddURL(AUrl);
  {$ELSE}
  AItem.Local := Trim(AUrl);
  AItem.Keyword := UTF8Trim(AText);
  {$ENDIF}
end;

procedure TWiki2CHMConverter.AddTocItem(ALevel: Integer; AText, AUrl: String);
// Is called whenever a new node is added to the xml TOC

  function NewItemAtLevel(ALevel: Integer): TChmSiteMapItem;
  var
    item: TChmSiteMapItem;
    items: TChmSiteMapItems;
    level: Integer;
  begin
    level := 0;
    items := FTOCSiteMap.Items;
    while level < ALevel do begin
      if items.Count = 0 then
        item := items.NewItem
      else
        item := items.Item[items.Count-1];
      items := item.Children;
      inc(level);
    end;
    Result := items.NewItem;
  end;

var
  item: TCHMSitemapItem;
begin
  item := NewItemAtLevel(ALevel);
  {$IF FPC_FULLVERSION>=30200}
  item.AddURL(AUrl);
  {$ELSE}
  item.Local := AUrl;
  {$ENDIF}
  item.Text := EscapeToHTML(AText);
  item.ImageNumber := 0;
end;

function CompareIndex(Item1, Item2: Pointer): Integer;
var
  indexItem1, indexItem2: TChmSiteMapItem;
begin
  indexItem1 := TChmSiteMapItem(Item1);
  indexItem2 := TChmSiteMapItem(Item2);
  Result := UTF8CompareStr(UTF8Lowercase(indexItem1.Text), UTF8Lowercase(indexItem2.Text));
end;

procedure TWiki2CHMConverter.OnWriterLastFileAdded(Sender: TObject);
var
  CurWriter: TChmWriter;
begin
  // Assign the TOC and index files
  CurWriter := TChmWriter(Sender);
  if CurWriter=nil then exit;

  // write Index (see TChmProject.LastFileAdded)
  if (IndexFileName <> '') then begin
    FIndexStream := TMemoryStream.Create;
    FIndexSiteMap.Items.Sort(@CompareIndex);
    FIndexSiteMap.SaveToStream(FIndexStream);
    CurWriter.AppendIndex(FIndexStream);
    CurWriter.AppendBinaryIndexFromSitemap(FIndexSitemap,false);
  end;

  // write TOC (see TChmProject.LastFileAdded)
  if (TOCFileName <> '') then begin
    FTOCStream := TMemoryStream.Create;
    FTocSitemap.SaveToStream(FTOCStream);
    CurWriter.AppendTOC(FTocStream);
    CurWriter.AppendBinaryTOCFromSiteMap(FTOCSitemap);
  end;
end;

function TWiki2CHMConverter.GetImageLink(ImgFilename: string): string;
begin
  Result:=CHMImagesDir+ExtractFileName(ImgFilename);
end;

function TWiki2CHMConverter.GetInternalImageLink(ImgFilename: String): String;
begin
  Result := CHMImagesDir + 'internal/' + ExtractFilename(ImgFilename);
end;

function TWiki2CHMConverter.GetPageLink(Page: TW2XHTMLPage): string;
begin
//  Result:=Page.WikiDocumentName+'.html';
  Result :=Page.Filename;
end;

function TWiki2CHMConverter.GetRelativeCSSFilename: String;
begin
  Result := ExtractFileName(CSSFilename);
end;

procedure TWiki2CHMConverter.SetCHMFile(AValue: string);
var
  NewValue: String;
begin
  NewValue:=TrimFilename(AValue);
  if FCHMFile=NewValue then Exit;
  FCHMFile:=NewValue;
end;

procedure TWiki2CHMConverter.SetIndexFileName(AValue: string);
begin
  if FIndexFileName=AValue then Exit;
  FIndexFileName:=AValue;
end;

procedure TWiki2CHMConverter.SetTOCFilename(AValue: String);
begin
  if FTOCFileName = AValue then Exit;
  FTOCFilename := AValue;
end;

procedure TWiki2CHMConverter.ConvertInit;
var
  i: Integer;
  Page: TW2CHMPage;
begin
  inherited ConvertInit;

  if CHMFile='' then
    raise Exception.Create('chm file not set');

  FIndexSitemap := TChmSitemap.Create(stIndex);
  FTOCSitemap := TChmSitemap.Create(stTOC);
  AddTocItem(0, FTOCRootName, '');

  for i:=0 to Count-1 do begin
    Page:=TW2CHMPage(Pages[i]);
    DocumentNameToPage[GetPageLink(Page)]:=Page;
  end;
end;

procedure TWiki2CHMConverter.SaveAllPages;
var
  ms: TMemoryStream;
  Filename: String;
  i: Integer;
  F2PItem: PStringToPointerTreeItem;
begin
  Filename:=CHMFile;
  debugln(['initializing chm ...']);
  Writer:=nil;
  ms:=TMemoryStream.Create;
  try
    Writer:=TChmWriter.Create(ms,false);
    // see TChmProject.WriteChm();

    // our callback to get data
    Writer.OnGetFileData :=@OnWriterGetFileData;
    Writer.OnLastFile    :=@OnWriterLastFileAdded;
    FilesCompressed:=0;

    // give it the html files
    for i:=0 to Count-1 do
      Writer.FilesToCompress.Add(GetPageLink(TW2CHMPage(Pages[i])));

    // give it the css file
    Writer.FilesToCompress.Add(GetRelativeCSSFilename);

    // give it the image files
    for F2PItem in UsedImages do
      Writer.FilesToCompress.Add(F2PItem^.Name);

    // now some settings in the chm
    Writer.DefaultPage := GetPageLink(TW2CHMPage(Pages[0]));
    Writer.Title := Title;
    //Writer.DefaultFont := DefaultFont;
    Writer.FullTextSearch := true;
    Writer.HasBinaryIndex := true;
    Writer.IndexName := IndexFileName;
    Writer.HasBinaryTOC := true;
    Writer.TOCName := FTOCFileName;
    //Writer.ReadmeMessage := ReadmeMessage;
    {for i:=0 to Count-1 do
      begin
        ContextNode:=TChmContextNode(files.objects[i]);
        if assigned(ContextNode) and (ContextNode.contextnumber<>0) then
          Writer.AddContext(ContextNode.ContextNumber,files[i]);
      end;}
    //if FWindows.Count>0 then
    //  Writer.Windows:=FWIndows;

    if assigned(FTocSiteMap) then
      Writer.TocSitemap := FTocSiteMap;

    // and write!
    debugln(['creating chm ...']);
    Writer.Execute;

    debugln(['writing file "',Filename,'" ...']);
    ms.SaveToFile(Filename);
  finally
    FreeAndNil(Writer);
    ms.Free;
  end;
end;

constructor TWiki2CHMConverter.Create;
begin
  inherited Create;
  FPageClass:=TW2CHMPage;
  FOutputDir:='chm';
  FCHMFile:='wiki.chm';
  FIndexFileName:='_index.hhk';
  FTOCFileName:='_table_of_contents.hhc';
  FTOCRootName:='Pages available';
  DocumentNameToPage:=TStringToPointerTree.Create(true);
end;

destructor TWiki2CHMConverter.Destroy;
begin
  FreeAndNil(FTocSiteMap);
  FreeAndNil(FTocStream);
  FreeAndNil(FIndexSitemap);
  FreeAndNil(FIndexStream);
  FreeAndNil(DocumentNameToPage);
  inherited Destroy;
end;

procedure TWiki2CHMConverter.Clear;
begin
  if DocumentNameToPage <> nil then
    DocumentNameToPage.Clear;
  inherited Clear;
end;

end.

