浏览代码

* redone sitemap to support multiple name,local pairs for index.
* rewritten binary index generation. Use natural sort for index.
* updated html scanning for ID= tags as anchor, reduces warnings with more modern html code.

Still work to do, ags helpfile still has 70 differences. Possibly however due to case sensitive anchors on index level.

git-svn-id: trunk@42124 -

marco 6 年之前
父节点
当前提交
3092b1169a

+ 1 - 0
packages/chm/fpmake.pp

@@ -31,6 +31,7 @@ begin
 
 
     D:=P.Dependencies.Add('fcl-xml');
     D:=P.Dependencies.Add('fcl-xml');
     D:=P.Dependencies.Add('fcl-base');
     D:=P.Dependencies.Add('fcl-base');
+    D:=P.Dependencies.Add('rtl-generics');
     D.Version:='3.3.1';
     D.Version:='3.3.1';
 
 
     P.SourcePath.Add('src');
     P.SourcePath.Add('src');

+ 2 - 3
packages/chm/src/chmcmd.lpr

@@ -145,7 +145,7 @@ begin
   else
   else
     begin
     begin
      try
      try
-      project.ScanHtmlContents:=htmlscan=scanforce;  // .hhp default SCAN
+      project.ScanHtmlContents:=htmlscan in [scanforce, scandefault];  // .hhp default SCAN
       Project.LoadFromFile(name);
       Project.LoadFromFile(name);
      except
      except
        on e:exception do
        on e:exception do
@@ -166,7 +166,6 @@ begin
     end;
     end;
   OutStream.Free;
   OutStream.Free;
   Project.Free;
   Project.Free;
-
 end;
 end;
 
 
 var
 var
@@ -178,7 +177,7 @@ var
 
 
 begin
 begin
   InitOptions;
   InitOptions;
-  Writeln(stderr,'chmcmd, a CHM compiler. (c) 2010 Free Pascal core.');
+  Writeln(stderr,'chmcmd, a CHM compiler. (c) 2010-2019 Free Pascal core.');
   Writeln(Stderr);
   Writeln(Stderr);
   repeat
   repeat
     c:=getlongopts('h',@theopts[1],optionindex);
     c:=getlongopts('h',@theopts[1],optionindex);

+ 112 - 72
packages/chm/src/chmfilewriter.pas

@@ -742,16 +742,19 @@ begin
 
 
    i:=pos('#',outstring);
    i:=pos('#',outstring);
    if i<>0 then begin
    if i<>0 then begin
-     if i > 1 then
-       Anchor := outstring
-     else
-       Anchor := localname+outstring;
-     j := fAnchorList.IndexOf(Anchor);
-     if j < 0 then begin
-       fAnchorList.AddObject(Anchor,TFirstReference.Create(localname));
-       Anchor := '(new) '+Anchor;
-     end;
-     Error(CHMNote, 'Anchor found '+Anchor+' while scanning '+localname,1);
+     if i<>length(outstring) then // trims lone '#' at end of url.
+       begin
+         if i > 1 then
+           Anchor := outstring
+         else
+           Anchor := localname+outstring;
+         j := fAnchorList.IndexOf(Anchor);
+         if j < 0 then begin
+           fAnchorList.AddObject(Anchor,TFirstReference.Create(localname));
+           Anchor := '(new) '+Anchor;
+         end;
+         Error(CHMNote, 'Anchor found '+Anchor+' while scanning '+localname,1);
+       end;
      delete(outstring,i,length(outstring)-i+1);
      delete(outstring,i,length(outstring)-i+1);
    end;
    end;
 
 
@@ -759,6 +762,8 @@ begin
 
 
   outstring:=extractrelativepath(basepath,outstring);
   outstring:=extractrelativepath(basepath,outstring);
   outstring:=StringReplace(outstring,'\','/',[rfReplaceAll]);
   outstring:=StringReplace(outstring,'\','/',[rfReplaceAll]);
+  if outstring='' then
+    result:=false;
 end;
 end;
 
 
 function  TChmProject.FileInTotalList(const s:String):boolean;
 function  TChmProject.FileInTotalList(const s:String):boolean;
@@ -808,13 +813,55 @@ begin
       filelist.add(fn);
       filelist.add(fn);
 end;
 end;
 
 
+procedure checkattributesA(node:TDomNode;const localname: string; filelist :TStringList);
+// workaround for "a" tag that has href and src. If src exists, don't check href, this
+// avoids spurious warnings.
+var
+    fn  : String;
+    val : String;
+    found : boolean;
+begin
+  found:=false;
+  val := findattribute(node,'SRC');
+  if sanitizeurl(fbasepath,val,localpath,localname,fn) then
+      found:=true;
+  if not found then
+    begin
+      val := findattribute(node,'HREF');
+      if sanitizeurl(fbasepath,val,localpath,localname,fn) then
+        found:=true;
+    end;
+ if found and not FileInTotalList(uppercase(fn)) then
+      filelist.add(fn);
+end;
 
 
 function scantags(prnt:TDomNode; const localname: string; filelist:TStringlist):TDomNode;
 function scantags(prnt:TDomNode; const localname: string; filelist:TStringlist):TDomNode;
-// Seach first matching tag in siblings
+
+var
+  att : ansistring;
+
+procedure AddAnchor(const s:string);
+var
+   i   : Integer;
+begin
+  i := fAnchorList.IndexOf(localname+'#'+s);
+  if i < 0 then begin
+    fAnchorList.Add(localname+'#'+s);
+    Error(ChmNote,'New Anchor with '+att+' '+s+' found while scanning '+localname,1);
+  end else if fAnchorList.Objects[i] = nil then
+    Error(chmwarning,'Duplicate anchor definitions with '+att+' '+s+' found while scanning '+localname,1)
+  else begin
+    fAnchorList.Objects[i].Free;
+    fAnchorList.Objects[i] := nil;
+    Error(ChmNote,'Anchor with '+att+' '+s+' defined while scanning '+localname,1);
+  end;
+end;
+
 var chld: TDomNode;
 var chld: TDomNode;
-    s,
-    att : ansistring;
-    i   : Integer;
+    s,attrval  : ansistring;
+    idfound : boolean;
+
+
 begin
 begin
   result:=nil;
   result:=nil;
   if assigned(prnt )  then
   if assigned(prnt )  then
@@ -826,6 +873,11 @@ begin
           if (chld is TDomElement) then
           if (chld is TDomElement) then
             begin
             begin
               s:=uppercase(tdomelement(chld).tagname);
               s:=uppercase(tdomelement(chld).tagname);
+              att := 'ID';
+              attrval := findattribute(chld, att);
+              idfound:=attrval  <> '' ;
+              if idfound then
+                addanchor(attrval);
               if s='LINK' then
               if s='LINK' then
                 begin
                 begin
                   //printattributes(chld,'');
                   //printattributes(chld,'');
@@ -836,34 +888,21 @@ begin
                   //printattributes(chld,'');
                   //printattributes(chld,'');
                   checkattributes(chld,'SRC',localname,filelist);
                   checkattributes(chld,'SRC',localname,filelist);
                 end;
                 end;
-             if s='IMG'then
+             if s='IMG' then
                begin
                begin
                   //printattributes(chld,'');
                   //printattributes(chld,'');
                   checkattributes(chld,'SRC',localname,filelist);
                   checkattributes(chld,'SRC',localname,filelist);
                end;
                end;
-             if s='A'then
+             if s='A' then
                begin
                begin
                   //printattributes(chld,'');
                   //printattributes(chld,'');
-                  checkattributes(chld,'HREF',localname,filelist);
-                  att := 'NAME';
-                  s := findattribute(chld, att);
-                  if s = '' then begin
-                     att := 'ID';
-                     s := findattribute(chld, att);
-                  end;
-                  if s <> '' then
+                  checkattributesA(chld,localname,filelist);
+                  if not idfound then
                     begin
                     begin
-                      i := fAnchorList.IndexOf(localname+'#'+s);
-                      if i < 0 then begin
-                        fAnchorList.Add(localname+'#'+s);
-                        Error(ChmNote,'New Anchor with '+att+' '+s+' found while scanning '+localname,1);
-                      end else if fAnchorList.Objects[i] = nil then
-                        Error(chmwarning,'Duplicate anchor definitions with '+att+' '+s+' found while scanning '+localname,1)
-                      else begin
-                        fAnchorList.Objects[i].Free;
-                        fAnchorList.Objects[i] := nil;
-                        Error(ChmNote,'Anchor with '+att+' '+s+' defined while scanning '+localname,1);
-                      end;
+                      att := 'NAME';
+                      attrval := findattribute(chld, att);
+                      if attrval  <> '' then
+                       addanchor(attrval);
                     end;
                     end;
                 end;
                 end;
             end;
             end;
@@ -876,7 +915,7 @@ var
   localfilelist: TStringList;
   localfilelist: TStringList;
   domdoc : THTMLDocument;
   domdoc : THTMLDocument;
   i,j    : Integer;
   i,j    : Integer;
-  fn,s   : string;
+  fn,reffn   : string;
   ext    : String;
   ext    : String;
   tmplst : Tstringlist;
   tmplst : Tstringlist;
   strrec : TStringIndex;
   strrec : TStringIndex;
@@ -926,10 +965,9 @@ begin
                scantags(domdoc,extractfilename(fn),localfilelist);
                scantags(domdoc,extractfilename(fn),localfilelist);
                for i:=0 to localFilelist.count-1 do
                for i:=0 to localFilelist.count-1 do
                  begin
                  begin
-                   s:=localfilelist[i];
-                   if not trypath(s) then
-//                     if not trypath(localpath+s) then
-                       Error(ChmWarning,'Found file '+s+' while scanning '+fn+', but couldn''t find it on disk',2);
+                   reffn:=localfilelist[i];
+                   if not trypath(reffn) then  //  if not trypath(localpath+s) then
+                       Error(ChmWarning,'Found file '+reffn+' while scanning '+fn+', but couldn''t find it on disk',2);
                  end;
                  end;
              except
              except
                on e:EDomError do
                on e:EDomError do
@@ -952,15 +990,14 @@ begin
 
 
            for i:=0 to tmplst.Count-1 do
            for i:=0 to tmplst.Count-1 do
              begin
              begin
-               s:=tmplst[i];
-               if pos('url(''', tmplst[i])>0 then
+               reffn:=tmplst[i];
+               if pos('url(''', reffn)>0 then
                  begin
                  begin
-                   delete(s,1,pos('url(''', tmplst[i])+4);
-                   s:=trim(copy(s,1,pos('''',s)-1));
-
-                   if not trypath(s) then
+                   delete(reffn,1,pos('url(''', reffn)+4);
+                   reffn:=trim(copy(reffn,1,pos('''',reffn)-1));
+                   if not trypath(reffn) then
 //                     if not trypath(localpath+s) then
 //                     if not trypath(localpath+s) then
-                       Error(ChmWarning,'Found file '+s+' while scanning '+fn+', but couldn''t find it on disk',2);
+                       Error(ChmWarning,'Found file '+reffn+' while scanning '+fn+', but couldn''t find it on disk',2);
                  end;
                  end;
              end;
              end;
          finally
          finally
@@ -984,8 +1021,9 @@ procedure TChmProject.ScanSitemap(sitemap:TChmSiteMap;newfiles:TStrings;recursio
 
 
 procedure scanitems(it:TChmSiteMapItems);
 procedure scanitems(it:TChmSiteMapItems);
 
 
-var i : integer;
+var i,j : integer;
     x : TChmSiteMapItem;
     x : TChmSiteMapItem;
+    si  : TChmSiteMapSubItem;
     s : string;
     s : string;
     strrec : TStringIndex;
     strrec : TStringIndex;
 
 
@@ -993,28 +1031,31 @@ begin
   for i:=0 to it.count -1 do
   for i:=0 to it.count -1 do
     begin
     begin
       x:=it.item[i];
       x:=it.item[i];
-      if sanitizeurl(fbasepath,x.local,'','Site Map for '+x.Text,S) then   // sanitize, remove stuff etc.
-        begin
-          if not FileInTotalList(uppercase(s)) then
-            begin
-              if fileexists(s) then
-                begin
-                  Error(chmnote,'Good url: '+s+'.',5);
-                  StrRec:=TStringIndex.Create;
-                  StrRec.TheString:=uppercase(s);
-                  StrRec.Strid    :=0;
-                  fTotalFileList.Add(StrRec);
-                  newfiles.add(s);
-                end
-              else
-                Error(chmnote,'duplicate url: '+s+'.',5);
-            end
-          else
-            Error(chmnote,'duplicate url: '+s+'.',5);
-        end
-      else
-       Error(chmnote,'Bad url: '+s+'.',5);
-
+      for j:=0 to x.SubItemcount-1 do
+         begin
+           si:=x.SubItem[j];
+           if sanitizeurl(fbasepath,si.local,'','Site Map for '+x.Text,S) then   // sanitize, remove stuff etc.
+             begin
+               if not FileInTotalList(uppercase(s)) then
+                 begin
+                   if fileexists(s) then
+                     begin
+                       Error(chmnote,'Good url: '+s+'.',5);
+                       StrRec:=TStringIndex.Create;
+                       StrRec.TheString:=uppercase(s);
+                       StrRec.Strid    :=0;
+                       fTotalFileList.Add(StrRec);
+                       newfiles.add(s);
+                     end
+                   else
+                     Error(chmnote,'duplicate url: '+s+'.',5);
+                 end
+               else
+                 Error(chmnote,'duplicate url: '+s+'.',5);
+             end
+           else
+            Error(chmnote,'Bad url: '+s+'.',5);
+         end;
       if assigned(x.children) and (x.children.count>0) then
       if assigned(x.children) and (x.children.count>0) then
         scanitems(x.children);
         scanitems(x.children);
     end;
     end;
@@ -1169,7 +1210,7 @@ var
 begin
 begin
   for i := 0 to fAnchorList.Count-1 do
   for i := 0 to fAnchorList.Count-1 do
     if fAnchorList.Objects[i] <> nil then
     if fAnchorList.Objects[i] <> nil then
-      Error(chmerror,'Anchor '+fAnchorList[i]+' undefined; first use '+TFirstReference(fAnchorList.Objects[i]).Location);
+       Error(chmerror,'Anchor '+fAnchorList[i]+' undefined; first use '+TFirstReference(fAnchorList.Objects[i]).Location);
 end;
 end;
 
 
 procedure TChmProject.LoadSitemaps;
 procedure TChmProject.LoadSitemaps;
@@ -1188,7 +1229,6 @@ begin
            FreeAndNil(FToc);
            FreeAndNil(FToc);
            FToc:=TChmSiteMap.Create(sttoc);
            FToc:=TChmSiteMap.Create(sttoc);
            FToc.loadfromstream(FTocStream);
            FToc.loadfromstream(FTocStream);
-           ftoc.savetofile('bla.something');
          except
          except
           on e:exception do
           on e:exception do
             begin
             begin

+ 183 - 96
packages/chm/src/chmreader.pas

@@ -20,15 +20,17 @@
 }
 }
 unit chmreader;
 unit chmreader;
 
 
-{$mode objfpc}{$H+}
+{$mode delphi}
 
 
 //{$DEFINE CHM_DEBUG}
 //{$DEFINE CHM_DEBUG}
 { $DEFINE CHM_DEBUG_CHUNKS}
 { $DEFINE CHM_DEBUG_CHUNKS}
-
+{define binindex}
+{define nonumber}
 interface
 interface
 
 
 uses
 uses
-  Classes, SysUtils, Contnrs, chmbase, paslzx, chmFIftiMain, chmsitemap;
+  Generics.Collections, Classes, SysUtils,  Contnrs,
+  chmbase, paslzx, chmFIftiMain, chmsitemap;
 
 
 type
 type
 
 
@@ -729,7 +731,7 @@ var
   PMGIndex: Integer;
   PMGIndex: Integer;
   {$ENDIF}
   {$ENDIF}
 begin
 begin
-  if ForEach = nil then Exit;
+  if not assigned(ForEach) then Exit;
   ChunkStream := TMemoryStream.Create;
   ChunkStream := TMemoryStream.Create;
   {$IFDEF CHM_DEBUG_CHUNKS}
   {$IFDEF CHM_DEBUG_CHUNKS}
   WriteLn('ChunkCount = ',fDirectoryHeader.DirectoryChunkCount);
   WriteLn('ChunkCount = ',fDirectoryHeader.DirectoryChunkCount);
@@ -970,6 +972,12 @@ begin
     fTOPICSStream.ReadDWord;
     fTOPICSStream.ReadDWord;
     TopicTitleOffset := LEtoN(fTOPICSStream.ReadDWord);
     TopicTitleOffset := LEtoN(fTOPICSStream.ReadDWord);
     TopicURLTBLOffset := LEtoN(fTOPICSStream.ReadDWord);
     TopicURLTBLOffset := LEtoN(fTOPICSStream.ReadDWord);
+    {$ifdef binindex}
+    {$ifndef nonumber}
+    writeln('titleid:',TopicTitleOffset);
+    writeln('urlid  :',TopicURLTBLOffset);
+    {$endif}
+    {$endif}
     if TopicTitleOffset <> $FFFFFFFF then
     if TopicTitleOffset <> $FFFFFFFF then
       ATitle := ReadStringsEntry(TopicTitleOffset);
       ATitle := ReadStringsEntry(TopicTitleOffset);
      //WriteLn('Got a title: ', ATitle);
      //WriteLn('Got a title: ', ATitle);
@@ -1016,7 +1024,10 @@ begin
   result:=head<tail;
   result:=head<tail;
 
 
   n:=head-oldhead;
   n:=head-oldhead;
-  if (n>0) and (oldhead[n-1]=0) then dec(n); // remove trailing #0
+
+  pw:=pword(@oldhead[n]);
+  if (n>1) and (pw[-1]=0) then
+    dec(n,2); // remove trailing #0
   setlength(ws,n div sizeof(widechar));
   setlength(ws,n div sizeof(widechar));
   move(oldhead^,ws[1],n);
   move(oldhead^,ws[1],n);
   for n:=1 to length(ws) do
   for n:=1 to length(ws) do
@@ -1024,10 +1035,15 @@ begin
   readv:=ws; // force conversion for now, and hope it doesn't require cwstring
   readv:=ws; // force conversion for now, and hope it doesn't require cwstring
 end;
 end;
 
 
+
+Type TLookupRec = record
+                   item : TChmSiteMapItems;
+                   depth : integer;
+                   end;
+     TLookupDict = TDictionary<string,TLookupRec>;
 function TChmReader.GetIndexSitemap(ForceXML:boolean=false): TChmSiteMap;
 function TChmReader.GetIndexSitemap(ForceXML:boolean=false): TChmSiteMap;
 var Index   : TMemoryStream;
 var Index   : TMemoryStream;
-    sitemap : TChmSiteMap;
-    Item    : TChmSiteMapItem;
+
 
 
 function  AbortAndTryTextual:tchmsitemap;
 function  AbortAndTryTextual:tchmsitemap;
 
 
@@ -1045,76 +1061,48 @@ begin
       result:=nil;
       result:=nil;
 end;
 end;
 
 
-procedure createentry(Name:ansistring;CharIndex:integer;Topic,Title:ansistring);
-var litem : TChmSiteMapItem;
-    shortname : ansistring;
-    longpart  : ansistring;
-begin
- if charindex=0 then
-   begin
-     item:=sitemap.items.NewItem;
-     item.keyword:=Name;
-     item.local:=topic;
-     item.text:=title;
-   end
- else
-   begin
-     shortname:=copy(name,1,charindex-2);
-     longpart:=copy(name,charindex,length(name)-charindex+1);
-     if assigned(item) and (shortname=item.text) then
-       begin
-         litem:=item.children.newitem;
-         litem.local:=topic;
-         litem.keyword :=longpart; // recursively split this? No examples.
-         litem.text:=title;
-       end
-      else
-       begin
-         item:=sitemap.items.NewItem;
-         item.keyword:=shortname;
-         item.local:=topic;
-         item.text:=title;
-         litem:=item.children.newitem;
-         litem.keyword:=longpart;
-         litem.local:=topic;
-         litem.text :=Title; // recursively split this? No examples.
-       end;
-   end;
-end;
+var
+   parentitem:TChmSiteMapItems;
+   itemstack :TObjectList;
+   lookup  : TLookupDict;
+   curitemdepth : integer;
+   sitemap : TChmSiteMap;
 
 
-procedure createentryseealso(Name:ansistring;CharIndex:integer;seealso:ansistring);
-var litem : TChmSiteMapItem;
+function getitem(anentrydepth:integer):Tchmsitemapitems;
 begin
 begin
-     item:=sitemap.items.NewItem;
-     item.KeyWord:=name;
-     item.SeeAlso:=seealso;
+   if anentrydepth<itemstack.count then
+     result:=tchmsitemapitems(itemstack[anentrydepth])
+   else
+     begin
+       {$ifdef binindex}
+         writeln('pop from emptystack at ',anentrydepth,' ',itemstack.count);
+       {$endif}
+       result:=tchmsitemapitems(itemstack[itemstack.Count-1]);
+     end;
 end;
 end;
 
 
+procedure pushitem(anentrydepth:integer;anitem:tchmsitemapitem);
+begin
 
 
+ if anentrydepth<itemstack.count then
+   itemstack[anentrydepth]:=anitem.children
+ else
+   if anentrydepth=itemstack.count then
+     itemstack.add(anitem.Children)
+   else
+     begin
+       {$ifdef binindex}
+         writeln('push more than 1 larger ' ,anentrydepth,' ',itemstack.count);
+       {$endif}
+       itemstack.add(anitem.Children)
+     end;
+end;
 procedure parselistingblock(p:pbyte);
 procedure parselistingblock(p:pbyte);
 var
 var
-    itemstack:TObjectStack;
-    curitemdepth : integer;
-    parentitem:TChmSiteMap;
 
 
-procedure updateparentitem(entrydepth:integer);
-begin
-  if entrydepth>curitemdepth then
-    begin
-      if curitemdepth<>0 then
-        itemstack.push(parentitem);
-      curitemdepth:=entrydepth;
-    end
-  else
-   if entrydepth>curitemdepth then
-    begin
-      if curitemdepth<>0 then
-        itemstack.push(parentitem);
-      curitemdepth:=entrydepth;
-    end
-end;
+    Item    : TChmSiteMapItem;
 
 
-var hdr:PBTreeBlockHeader;
+    hdr:PBTreeBlockHeader;
     head,tail : pbyte;
     head,tail : pbyte;
     isseealso,
     isseealso,
     entrydepth,
     entrydepth,
@@ -1125,8 +1113,41 @@ var hdr:PBTreeBlockHeader;
     CharIndex,
     CharIndex,
     ind:integer;
     ind:integer;
     seealsostr,
     seealsostr,
-    topic,
+    s,
     Name : AnsiString;
     Name : AnsiString;
+    path,
+    shortname : AnsiString;
+    anitem:TChmSiteMapItems;
+    litem : TChmSiteMapItem;
+    lookupitem : TLookupRec;
+
+function readvalue:string;
+begin
+  if head<tail Then
+    begin
+      ind:=LEToN(plongint(head)^);
+
+      result:=lookuptopicbyid(ind,title);
+      {$ifdef binindex}
+        writeln(i:3,' topic: ' {$ifndef nonumber},'  (',ind,')' {$endif});
+        writeln('    title: ',title);
+        writeln('    result: ',result);
+      {$endif}
+      inc(head,4);
+    end;
+end;
+
+procedure dumpstack;
+var fp : TChmSiteMapItems;
+     ix : Integer;
+begin
+  for ix:=0 to itemstack.Count-1 do
+    begin
+      fp :=TChmSiteMapItems(itemstack[ix]);
+      writeln(ix:3,' ',fp.parentname);
+    end;
+end;
+
 begin
 begin
   //setlength (curitem,10);
   //setlength (curitem,10);
   hdr:=PBTreeBlockHeader(p);
   hdr:=PBTreeBlockHeader(p);
@@ -1135,17 +1156,21 @@ begin
   hdr^.IndexOfPrevBlock:=LEToN(hdr^.IndexOfPrevBlock);
   hdr^.IndexOfPrevBlock:=LEToN(hdr^.IndexOfPrevBlock);
   hdr^.IndexOfNextBlock:=LEToN(hdr^.IndexOfNextBlock);
   hdr^.IndexOfNextBlock:=LEToN(hdr^.IndexOfNextBlock);
 
 
+  {$ifdef binindex}
+  writeln('hdr:',hdr^.length);
+  {$endif}
   tail:=p+(2048-hdr^.length);
   tail:=p+(2048-hdr^.length);
   head:=p+sizeof(TBtreeBlockHeader);
   head:=p+sizeof(TBtreeBlockHeader);
 
 
-  itemstack:=TObjectStack.create;
   {$ifdef binindex}
   {$ifdef binindex}
+  {$ifndef nonumber}
   writeln('previndex  : ',hdr^.IndexOfPrevBlock);
   writeln('previndex  : ',hdr^.IndexOfPrevBlock);
   writeln('nextindex  : ',hdr^.IndexOfNextBlock);
   writeln('nextindex  : ',hdr^.IndexOfNextBlock);
   {$endif}
   {$endif}
-  curitemdepth:=0;
+  {$endif}
   while head<tail do
   while head<tail do
     begin
     begin
+      //writeln(tail-head);
       if not ReadWCharString(Head,Tail,Name) Then
       if not ReadWCharString(Head,Tail,Name) Then
         Break;
         Break;
       {$ifdef binindex}
       {$ifdef binindex}
@@ -1158,6 +1183,75 @@ begin
       IsSeealso:=LEToN(PE^.isseealso);
       IsSeealso:=LEToN(PE^.isseealso);
       EntryDepth:=LEToN(PE^.entrydepth);
       EntryDepth:=LEToN(PE^.entrydepth);
       CharIndex:=LEToN(PE^.CharIndex);
       CharIndex:=LEToN(PE^.CharIndex);
+      Path:='';
+
+      if charindex<>0 then
+        begin
+          Path:=Trim(Copy(Name,1,charindex-2));
+          Shortname:=trim(copy(Name,charindex,Length(Name)-Charindex+1));
+        end
+      else
+        shortname:=name;
+      {$ifdef binindex}
+      writeln('depth:', curitemdepth, ' ' ,entrydepth);
+      {$endif}
+      if curitemdepth=entrydepth then // same level, so of same parent
+         begin
+           item:=parentitem.newitem;
+           pushitem(entrydepth+1,item);
+         end
+      else
+        if curitemdepth=entrydepth-1 then // new child, one lower.
+          begin
+            parentitem:=getitem(entrydepth);
+            item:=parentitem.newitem;
+            pushitem(entrydepth+1,item);
+          end
+        else
+         if entrydepth<curitemdepth then
+          begin
+            parentitem:=getitem(entrydepth);
+            {$ifdef binindex}
+            writeln('bingo!', parentitem.parentname);
+            dumpstack;
+            {$endif}
+            item:=parentitem.newitem;
+            pushitem(entrydepth+1,item);
+          end;
+
+      curitemdepth:=entrydepth;
+      {$ifdef binindex}
+      writeln('lookup:', Name, ' = ', path,' = ',shortname);
+      {$endif}
+
+    (*  if lookup.trygetvalue(path,lookupitem) then
+        begin
+//          if lookupitem.item<>parentitem then
+//             writeln('mismatch: ',lookupitem.item.item[0].name,' ',name);
+{          if curitemdepth<entrydepth then
+            begin
+              writeln('lookup ok!',curitemdepth,' ' ,entrydepth);
+              curitemdepth:=entrydepth;
+            end
+          else
+           begin
+             writeln('lookup odd!',curitemdepth,' ' ,entrydepth);
+           end;
+          curitemdepth:=lookupitem.depth+1;
+          parentitem:=lookupitem.item;}
+        end
+      else
+        begin
+ //            parentitem:=sitemap.Items;
+          if not curitemdepth=entrydepth then
+             writeln('no lookup odd!',curitemdepth,' ' ,entrydepth);
+        end;  *)
+{      item:=parentitem.newitem;}
+      lookupitem.item:=item.children;
+      lookupitem.depth:=entrydepth;
+      lookup.addorsetvalue(name,lookupitem);
+      item.AddName(Shortname);
+
       {$ifdef binindex}
       {$ifdef binindex}
         Writeln('seealso   :  ',IsSeeAlso);
         Writeln('seealso   :  ',IsSeeAlso);
         Writeln('entrydepth:  ',EntryDepth);
         Writeln('entrydepth:  ',EntryDepth);
@@ -1178,7 +1272,7 @@ begin
           {$ifdef binindex}
           {$ifdef binindex}
             writeln('seealso: ',seealsostr);
             writeln('seealso: ',seealsostr);
           {$endif}
           {$endif}
-
+          item.AddSeeAlso(seealsostr);
         end
         end
       else
       else
         begin
         begin
@@ -1190,24 +1284,13 @@ begin
 
 
             for i:=0 to nrpairs-1 do
             for i:=0 to nrpairs-1 do
               begin
               begin
-                if head<tail Then
-                  begin
-                    ind:=LEToN(plongint(head)^);
-                    topic:=lookuptopicbyid(ind,title);
-                    {$ifdef binindex}
-                      writeln(i:3,' topic: ',topic);
-                      writeln('    title: ',title);
-                    {$endif}
-                    inc(head,4);
-                  end;
+               s:=readvalue;
+             //  if not ((i=0) and (title=shortname)) then
+               item.addname(title);
+               item.addlocal(s);
               end;
               end;
           end;
           end;
          end;
          end;
-      if isseealso>0 then
-         createentryseealso(name,charindex,seealsostr)
-      else
-        if nrpairs<>0 Then
-          createentry(Name,CharIndex,Topic,Title);
       inc(head,4); // always 1
       inc(head,4); // always 1
       {$ifdef binindex}
       {$ifdef binindex}
         if head<tail then
         if head<tail then
@@ -1215,15 +1298,16 @@ begin
       {$endif}
       {$endif}
       inc(head,4); // zero based index (13 higher than last
       inc(head,4); // zero based index (13 higher than last
     end;
     end;
-  ItemStack.Free;
 end;
 end;
 
 
 var TryTextual : boolean;
 var TryTextual : boolean;
     BHdr       : TBTreeHeader;
     BHdr       : TBTreeHeader;
     block      : Array[0..2047] of Byte;
     block      : Array[0..2047] of Byte;
     i          : Integer;
     i          : Integer;
+
 begin
 begin
    Result := nil;  SiteMap:=Nil;
    Result := nil;  SiteMap:=Nil;
+   lookup:=TDictionary<string,TLookupRec>.create;
    // First Try Binary
    // First Try Binary
    Index := GetObject('/$WWKeywordLinks/BTree');
    Index := GetObject('/$WWKeywordLinks/BTree');
    if (Index = nil) or ForceXML then
    if (Index = nil) or ForceXML then
@@ -1237,9 +1321,12 @@ begin
      Exit;
      Exit;
    end;
    end;
    SiteMap:=TChmSitemap.Create(StIndex);
    SiteMap:=TChmSitemap.Create(StIndex);
-   Item   :=Nil;  // cached last created item, in case we need to make
+   itemstack :=TObjectList.create(false);
+   //Item   :=Nil;  // cached last created item, in case we need to make
                   // a child.
                   // a child.
-
+   parentitem:=sitemap.Items;
+   itemstack.add(parentitem); // level 0
+   curitemdepth:=0;
    TryTextual:=True;
    TryTextual:=True;
    BHdr.LastLstBlock:=0;
    BHdr.LastLstBlock:=0;
    if LoadBtreeHeader(index,BHdr) and (BHdr.LastLstBlock>=0) Then
    if LoadBtreeHeader(index,BHdr) and (BHdr.LastLstBlock>=0) Then
@@ -1248,7 +1335,7 @@ begin
          begin
          begin
            for i:=0 to BHdr.lastlstblock do
            for i:=0 to BHdr.lastlstblock do
              begin
              begin
-               if (index.size-index.position)>=defblocksize then
+               if (index.size-index.position)>=defblocksize then // skips last incomplete block?
                  begin
                  begin
                    Index.read(block,defblocksize);
                    Index.read(block,defblocksize);
                    parselistingblock(@block)
                    parselistingblock(@block)
@@ -1264,6 +1351,7 @@ begin
       Result:=AbortAndTryTextual;
       Result:=AbortAndTryTextual;
     end
     end
   else Index.Free;
   else Index.Free;
+  lookup.free;
 end;
 end;
 
 
 function TChmReader.GetTOCSitemap(ForceXML:boolean=false): TChmSiteMap;
 function TChmReader.GetTOCSitemap(ForceXML:boolean=false): TChmSiteMap;
@@ -1279,13 +1367,12 @@ function TChmReader.GetTOCSitemap(ForceXML:boolean=false): TChmSiteMap;
       Item := SiteMapITems.NewItem;
       Item := SiteMapITems.NewItem;
       Props := LEtoN(TOC.ReadDWord);
       Props := LEtoN(TOC.ReadDWord);
       if (Props and TOC_ENTRY_HAS_LOCAL) = 0 then
       if (Props and TOC_ENTRY_HAS_LOCAL) = 0 then
-        Item.Text:= ReadStringsEntry(LEtoN(TOC.ReadDWord))
+        Item.AddName(ReadStringsEntry(LEtoN(TOC.ReadDWord)))
       else
       else
       begin
       begin
         TopicsIndex := LEtoN(TOC.ReadDWord);
         TopicsIndex := LEtoN(TOC.ReadDWord);
-        Item.Local := LookupTopicByID(TopicsIndex, Title);
-        Item.Text := Title;
-
+        Item.AddName(title);
+        Item.addLocal(LookupTopicByID(TopicsIndex, Title));
       end;
       end;
       TOC.ReadDWord;
       TOC.ReadDWord;
       Result := LEtoN(TOC.ReadDWord);
       Result := LEtoN(TOC.ReadDWord);
@@ -1724,7 +1811,7 @@ var
   X: Integer;
   X: Integer;
 begin
 begin
   fOnOpenNewFile := AValue;
   fOnOpenNewFile := AValue;
-  if AValue = nil then exit;
+  if not assigned(AValue)  then exit;
   for X := 0 to fUnNotifiedFiles.Count-1 do
   for X := 0 to fUnNotifiedFiles.Count-1 do
     AValue(Self, X);
     AValue(Self, X);
   fUnNotifiedFiles.Clear;
   fUnNotifiedFiles.Clear;

+ 405 - 149
packages/chm/src/chmsitemap.pas

@@ -20,54 +20,104 @@
 }
 }
 unit chmsitemap;
 unit chmsitemap;
 
 
-{$mode objfpc}{$H+}
-
+{$mode Delphi}{$H+}
+{define preferlower}
 interface
 interface
 
 
 uses
 uses
-  Classes, SysUtils, fasthtmlparser;
+  Classes, SysUtils, fasthtmlparser, contnrs, strutils, generics.collections;
 
 
 type
 type
   TChmSiteMapItems = class; // forward
   TChmSiteMapItems = class; // forward
   TChmSiteMap = class;
   TChmSiteMap = class;
+  TChmSiteMapItem = class;
 
 
   { TChmSiteMapItem }
   { TChmSiteMapItem }
 
 
+  TChmSiteMapItemAttrName = (siteattr_NONE,
+                             siteattr_KEYWORD, // alias for name in sitemap
+                             siteattr_NAME,
+                             siteattr_LOCAL,
+                             siteattr_URL,
+                             siteattr_TYPE,
+                             siteattr_SEEALSO,
+                             siteattr_IMAGENUMBER,
+                             siteattr_NEW,
+                             siteattr_COMMENT,
+                             siteattr_MERGE,
+                             siteattr_FRAMENAME,
+                             siteattr_WINDOWNAME,
+                             siteattr_WINDOW_STYLES,
+                             siteattr_EXWINDOW_STYLES,
+                             siteattr_FONT,
+                             siteattr_IMAGELIST,
+                             siteattr_IMAGETYPE
+                            );
+
+  { TChmSiteMapSubItem }
+  TChmSiteMapGenerationOptions = (Default,emitkeyword);
+  TChmSiteMapSubItem = class(TPersistent)
+  private
+    FName,
+    FType,
+    FLocal,
+    FUrl,
+    FSeeAlso  : String;
+    FOwner : TChmSiteMapItem;
+  public
+    constructor Create(AOwner: TChmSiteMapItem);
+    destructor Destroy; override;
+  published
+    property Name : String read FName  write FName;  //hhk
+    property ItemType : String read FType write FType; //both
+    property Local: String read FLocal write FLocal; //both
+    property URL  : String read FURL write FURL;     //both
+    property SeeAlso: String read FSeeAlso write FSeeAlso; //hhk
+  end;
+
+// hhk items:  Merge | ([Name] ([Name] [Type...] [Local [URL] | See Also])... [FrameName] [WindowName] [Comment])
+// hhc items:  Merge | ([Name] ([Type...] [Local] [URL])... [FrameName] [WindowName] [Comment] [New] [ImageNumber])
   TChmSiteMapItem = class(TPersistent)
   TChmSiteMapItem = class(TPersistent)
   private
   private
     FChildren: TChmSiteMapItems;
     FChildren: TChmSiteMapItems;
     FComment: String;
     FComment: String;
     FImageNumber: Integer;
     FImageNumber: Integer;
     FIncreaseImageIndex: Boolean;
     FIncreaseImageIndex: Boolean;
-    FKeyWord: String;
-    FLocal: String;
     FOwner: TChmSiteMapItems;
     FOwner: TChmSiteMapItems;
-    FSeeAlso: String;
-    FText: String;
-    FURL: String;
+    FName   : String;
     FMerge : String;
     FMerge : String;
     FFrameName : String;
     FFrameName : String;
     FWindowName : String;
     FWindowName : String;
+    FSubItems : TObjectList;
+    function getlocal: string;
+    function getseealso:string;
+    function getsubitem( index : integer): TChmSiteMapSubItem;
+    function getsubitemcount: integer;
     procedure SetChildren(const AValue: TChmSiteMapItems);
     procedure SetChildren(const AValue: TChmSiteMapItems);
   public
   public
     constructor Create(AOwner: TChmSiteMapItems);
     constructor Create(AOwner: TChmSiteMapItems);
     destructor Destroy; override;
     destructor Destroy; override;
+    procedure AddName(const Name:string);
+    procedure AddLocal(const Local:string);
+    procedure AddSeeAlso(const SeeAlso:string);
+    procedure AddURL(const URL:string);
+    procedure AddType(const AType:string);
+    procedure Sort(Compare: TListSortCompare);
   published
   published
     property Children: TChmSiteMapItems read FChildren write SetChildren;
     property Children: TChmSiteMapItems read FChildren write SetChildren;
-    property Text: String read FText write FText; // Name for TOC; KeyWord for index
-    property KeyWord: String read FKeyWord write FKeyWord;
-    property Local: String read FLocal write FLocal;
-    property URL: String read FURL write FURL;
-    property SeeAlso: String read FSeeAlso write FSeeAlso;
+    property Name: String read FName write FName;
     property ImageNumber: Integer read FImageNumber write FImageNumber default -1;
     property ImageNumber: Integer read FImageNumber write FImageNumber default -1;
     property IncreaseImageIndex: Boolean read FIncreaseImageIndex write FIncreaseImageIndex;
     property IncreaseImageIndex: Boolean read FIncreaseImageIndex write FIncreaseImageIndex;
     property Comment: String read FComment write FComment;
     property Comment: String read FComment write FComment;
     property Owner: TChmSiteMapItems read FOwner;
     property Owner: TChmSiteMapItems read FOwner;
-
+    property Local : string read getlocal; // deprecated;             // should work on ALL pairs
+    property Text : string read fname write fname; // deprecated;     // should work on ALL pairs
+    property SeeAlso : string read getseealso; // deprecated;     // should work on ALL pairs
     property FrameName: String read FFrameName write FFrameName;
     property FrameName: String read FFrameName write FFrameName;
     property WindowName: String read FWindowName write FWindowName;
     property WindowName: String read FWindowName write FWindowName;
-//    property Type_: Integer read FType_ write FType_; either Local or URL
     property Merge: String read FMerge write FMerge;
     property Merge: String read FMerge write FMerge;
+    property SubItem[ index :integer]:TChmSiteMapSubItem read getsubitem;
+    property SubItemcount  :integer read getsubitemcount;
   end;
   end;
 
 
   { TChmSiteMapItems }
   { TChmSiteMapItems }
@@ -80,6 +130,7 @@ type
     FParentItem: TChmSiteMapItem;
     FParentItem: TChmSiteMapItem;
     function GetCount: Integer;
     function GetCount: Integer;
     function GetItem(AIndex: Integer): TChmSiteMapItem;
     function GetItem(AIndex: Integer): TChmSiteMapItem;
+    function getparentname: String;
     procedure SetItem(AIndex: Integer; const AValue: TChmSiteMapItem);
     procedure SetItem(AIndex: Integer; const AValue: TChmSiteMapItem);
   public
   public
     constructor Create(AOwner: TChmSiteMap; AParentItem: TChmSiteMapItem);
     constructor Create(AOwner: TChmSiteMap; AParentItem: TChmSiteMapItem);
@@ -95,6 +146,7 @@ type
     property ParentItem: TChmSiteMapItem read FParentItem;
     property ParentItem: TChmSiteMapItem read FParentItem;
     property Owner: TChmSiteMap read FOwner;
     property Owner: TChmSiteMap read FOwner;
     property InternalData: Dword read FInternalData write FInternalData;
     property InternalData: Dword read FInternalData write FInternalData;
+    property ParentName : String read getparentname;
   end;
   end;
   
   
 
 
@@ -130,13 +182,17 @@ type
     FLevel: Integer;
     FLevel: Integer;
     FLevelForced: Boolean;
     FLevelForced: Boolean;
     FWindowStyles: LongInt;
     FWindowStyles: LongInt;
+    FLoadDict : TDictionary<String,TChmSiteMapItemAttrName>;
+    fChmSiteMapGenerationOptions:TChmSiteMapGenerationOptions;
     procedure SetItems(const AValue: TChmSiteMapItems);
     procedure SetItems(const AValue: TChmSiteMapItems);
+    procedure CheckLookup;
   protected
   protected
     procedure FoundTag (ACaseInsensitiveTag, AActualTag: string);
     procedure FoundTag (ACaseInsensitiveTag, AActualTag: string);
     procedure FoundText(AText: string);
     procedure FoundText(AText: string);
   public
   public
     constructor Create(AType: TSiteMapType);
     constructor Create(AType: TSiteMapType);
     destructor Destroy; override;
     destructor Destroy; override;
+    Procedure Sort(Compare: TListSortCompare);
     procedure LoadFromFile(AFileName: String);
     procedure LoadFromFile(AFileName: String);
     procedure LoadFromStream(AStream: TStream);
     procedure LoadFromStream(AStream: TStream);
     procedure SaveToFile(AFileName:String);
     procedure SaveToFile(AFileName:String);
@@ -155,11 +211,50 @@ type
     property UseFolderImages: Boolean read FUseFolderImages write FUseFolderImages;
     property UseFolderImages: Boolean read FUseFolderImages write FUseFolderImages;
     property Font: String read FFont write FFont;
     property Font: String read FFont write FFont;
     property AutoGenerated: Boolean read FAutoGenerated write FAutoGenerated;
     property AutoGenerated: Boolean read FAutoGenerated write FAutoGenerated;
+    property ChmSiteMapGenerationOptions : TChmSiteMapGenerationOptions read fChmSiteMapGenerationOptions write fChmSiteMapGenerationOptions;
   end;
   end;
 
 
+
+function indexitemcompare(Item1, Item2: Pointer): Integer;
 implementation
 implementation
 uses HTMLUtil;
 uses HTMLUtil;
 
 
+const sitemapkws : array[TChmSiteMapItemAttrName] of string = (
+                    '',
+                    'KEYWORD',
+                    'NAME',
+                    'LOCAL',
+                    'URL',
+                    'TYPE',
+                    'SEE ALSO',
+                    'IMAGENUMBER',
+                    'NEW',
+                    'COMMENT',
+                    'MERGE',
+                    'FRAMENAME',
+                    'WINDOWNAME',
+                    'WINDOW STYLES',
+                    'EXWINDOW STYLES',
+                    'FONT',
+                    'IMAGELIST',
+                    'IMAGETYPE');
+
+function indexitemcompare(Item1, Item2: Pointer): Integer;
+begin
+    Result := naturalComparetext(LowerCase(TChmSiteMapItem(item1).name), Lowercase(TChmSiteMapItem(item2).name));
+end;
+{ TChmSiteMapSubItem }
+
+constructor TChmSiteMapSubItem.Create(AOwner: TChmSiteMapItem);
+begin
+  FOwner:=AOwner;
+end;
+
+destructor TChmSiteMapSubItem.Destroy;
+begin
+  inherited Destroy;
+end;
+
 { TChmSiteMapTree }
 { TChmSiteMapTree }
 
 
 procedure TChmSiteMap.SetItems(const AValue: TChmSiteMapItems);
 procedure TChmSiteMap.SetItems(const AValue: TChmSiteMapItems);
@@ -168,6 +263,16 @@ begin
   FItems:=AValue;
   FItems:=AValue;
 end;
 end;
 
 
+procedure TChmSiteMap.CheckLookup;
+var en : TChmSiteMapItemAttrName;
+begin
+  if assigned(FLoadDict) then
+    exit;
+  FLoadDict :=TDictionary<String,TChmSiteMapItemAttrName>.Create;
+  for en:=succ(low(en)) to high(en) do
+    FLoadDict.add(sitemapkws[en],en);
+end;
+
 procedure TChmSiteMap.FoundTag(ACaseInsensitiveTag, AActualTag: string);
 procedure TChmSiteMap.FoundTag(ACaseInsensitiveTag, AActualTag: string);
     procedure NewSiteMapItem;
     procedure NewSiteMapItem;
     begin
     begin
@@ -196,131 +301,98 @@ procedure TChmSiteMap.FoundTag(ACaseInsensitiveTag, AActualTag: string);
       else FCurrentItems := nil;
       else FCurrentItems := nil;
       Dec(FLevel);
       Dec(FLevel);
     end;
     end;
+
+// hhk items:  Merge | ([Name] ([Name] [Type...] [Local [URL] | See Also])... [FrameName] [WindowName] [Comment])
+// hhc items:  Merge | ([Name] ([Type...] [Local] [URL])... [FrameName] [WindowName] [Comment] [New] [ImageNumber])
 var
 var
   TagName,
   TagName,
-  //TagAttribute,
   TagAttributeName,
   TagAttributeName,
   TagAttributeValue: String;
   TagAttributeValue: String;
   isParam,IsMerged : string;
   isParam,IsMerged : string;
+  TagAttrName  : TChmSiteMapItemAttrName;
 begin
 begin
-  //WriteLn('TAG:', AActualTag);
   TagName := GetTagName(ACaseInsensitiveTag);
   TagName := GetTagName(ACaseInsensitiveTag);
-
-{  if not (smtHTML in FSiteMapTags) then begin
-    if (TagName = 'HTML') or (TagName = '/HTML') then Include(FSiteMapTags, smtHTML);
-  end
-  else begin // looking for /HTML
-    if TagName = '/HTML' then Exclude(FSiteMapTags, smtHTML);
-  end;}
-
-  //if (smtHTML in FSiteMapTags) then begin
-     if not (smtBODY in FSiteMapTags) then begin
-       if TagName = 'BODY' then Include(FSiteMapTags, smtBODY);
-     end
-     else begin
-       if TagName = '/BODY' then Exclude(FSiteMapTags, smtBODY);
+   if TagName = 'UL' then begin
+     IncreaseULevel;
+   end
+   else if TagName = '/UL' then begin
+     DecreaseULevel;
+   end
+   else if (TagName = 'LI') and (FLevel = 0) then
+     FLevelForced := True
+   else if TagName = 'OBJECT' then begin
+     Include(FSiteMapBodyTags, smbtOBJECT);
+     if FLevelForced then
+       IncreaseULevel;
+     If FLevel > 0 then // if it is zero it is the site properties
+       NewSiteMapItem;
+   end
+   else if TagName = '/OBJECT' then begin
+     Exclude(FSiteMapBodyTags, smbtOBJECT);
+     if FLevelForced then
+     begin
+       DecreaseULevel;
+       FLevelForced := False;
      end;
      end;
-
-     if (smtBODY in FSiteMapTags) then begin
-       //WriteLn('GOT TAG: ', AActualTag);
-       if TagName = 'UL' then begin
-         //WriteLN('Inc Level');
-         IncreaseULevel;
-       end
-       else if TagName = '/UL' then begin
-         //WriteLN('Dec Level');
-         DecreaseULevel;
-       end
-       else if (TagName = 'LI') and (FLevel = 0) then
-         FLevelForced := True
-       else if TagName = 'OBJECT' then begin
-         Include(FSiteMapBodyTags, smbtOBJECT);
-         if FLevelForced then
-           IncreaseULevel;
-         If FLevel > 0 then // if it is zero it is the site properties
-           NewSiteMapItem;
-       end
-       else if TagName = '/OBJECT' then begin
-         Exclude(FSiteMapBodyTags, smbtOBJECT);
-         if FLevelForced then
+   end
+   else begin // we are the properties of the object tag
+     if (smbtOBJECT in FSiteMapBodyTags) then
+       begin
+        if (FLevel > 0 ) then
          begin
          begin
-           DecreaseULevel;
-           FLevelForced := False;
-         end;
-       end
-       else begin // we are the properties of the object tag
-         if (smbtOBJECT in FSiteMapBodyTags) then
-           begin
-            if (FLevel > 0 ) then 
-             begin
-                if LowerCase(GetTagName(AActualTag)) = 'param' then begin
-                  TagAttributeName := GetVal(AActualTag, 'name');
-                TagAttributeValue := GetVal(AActualTag, 'value');
-                //writeln('name,value',tagattributename, ' ',tagattributevalue);
-                if TagAttributeName <> '' then begin
-                  if CompareText(TagAttributeName, 'keyword') = 0 then begin
-                    ActiveItem.Text := TagAttributeValue;
-                  end
-                  else if CompareText(TagAttributeName, 'name') = 0 then begin
-                    if ActiveItem.Text = '' then ActiveItem.Text := TagAttributeValue;
-                  end
-                  else if CompareText(TagAttributeName, 'local') = 0 then begin
-                    ActiveItem.Local := TagAttributeValue;
-                  end
-                  else if CompareText(TagAttributeName, 'URL') = 0 then begin
-                    ActiveItem.URL := TagAttributeValue;
-                  end
-                  else if CompareText(TagAttributeName, 'ImageNumber') = 0 then begin
-                    ActiveItem.ImageNumber := StrToInt(TagAttributeValue);
-                  end
-                  else if CompareText(TagAttributeName, 'New') = 0 then begin
-                    ActiveItem.IncreaseImageIndex := (LowerCase(TagAttributeValue) = 'yes');
-                  end
-                  else if CompareText(TagAttributeName, 'Comment') = 0 then begin
-                    ActiveItem.Comment := TagAttributeValue
-                  end
-                  else if CompareText(TagAttributeName, 'Merge') = 0 then begin
-                    ActiveItem.Merge:= TagAttributeValue
-                  end;
-                  //else if CompareText(TagAttributeName, '') = 0 then begin
-                  //end;
-                end;
-              end;
-            end
-           else
-             begin // object and level is zero?
-               if LowerCase(GetTagName(AActualTag)) = 'param' then begin
-                 begin
-                   TagAttributeName := uppercase(GetVal(AActualTag, 'name'));
-                   TagAttributeValue := GetVal(AActualTag, 'value');
-                   if TagAttributeName = 'FRAMENAME' then
-                     framename:=TagAttributeValue
-                   else
-                     if TagAttributeName = 'WINDOWNAME' then
-                       WINDOWname:=TagAttributeValue
-                   else
-                     if TagAttributeName = 'WINDOW STYLES' then
-                       WindowStyles:=StrToIntDef(TagAttributeValue,0)
-                   else
-                     if TagAttributeName = 'EXWINDOW STYLES' then
-                       ExWindowStyles:=StrToIntDef(TagAttributeValue,0)
-                   else
-                     if TagAttributeName = 'FONT' then
-                       FONT:=TagAttributeValue
-                   else
-                     if TagAttributeName = 'IMAGELIST' then
-                      IMAGELIST:=TagAttributeValue
-                    else
-                     if TagAttributeName = 'IMAGETYPE' then
-                      UseFolderImages:=uppercase(TagAttributeValue)='FOLDER';
-                  // writeln('0:',flevel,' ' ,aactualtag,' ',tagname,' ' ,tagattributename, ' ' ,tagattributevalue);
-                 end;
+            if LowerCase(GetTagName(AActualTag)) = 'param' then begin
+              TagAttributeName := GetVal(AActualTag, 'name');
+              TagAttributeValue := GetVal(AActualTag, 'value');
+
+              // a hash reduces comparisons and casing, and generics make it easy.
+              if not FLoadDict.trygetvalue(uppercase(TagAttributeName),TagAttrName) then
+                 TagAttrName:=siteattr_none;
+
+              if TagAttrName <> siteattr_none then begin
+                 case TagAttrName of
+                 siteattr_KEYWORD,
+                 siteattr_NAME         : Activeitem.AddName(TagAttributeValue);
+                 siteattr_LOCAL        : ActiveItem.AddLocal(TagAttributeValue);
+                 siteattr_URL          : ActiveItem.AddURL (TagAttributeValue);
+                 siteattr_TYPE         : ActiveItem.AddType (TagAttributeValue);
+                 siteattr_SEEALSO      : ActiveItem.AddSeeAlso(TagAttributeValue);
+                 siteattr_IMAGENUMBER  : ActiveItem.ImageNumber := StrToInt(TagAttributeValue);
+                 siteattr_NEW          : ActiveItem.IncreaseImageIndex := (LowerCase(TagAttributeValue) = 'yes');
+                 siteattr_COMMENT      : ActiveItem.Comment := TagAttributeValue;
+                 siteattr_MERGE        : ActiveItem.Merge:= TagAttributeValue;
+                 siteattr_FRAMENAME    : ActiveItem.FrameName:=TagAttributeValue;
+                 siteattr_WINDOWNAME   : ActiveItem.WindowName:=TagAttributeValue;
                  end;
                  end;
+              end;
+            end;
+         end
+       else
+         begin // object and level is zero?
+           if LowerCase(GetTagName(AActualTag)) = 'param' then begin
+             begin
+               TagAttributeName := uppercase(GetVal(AActualTag, 'name'));
+               TagAttributeValue := GetVal(AActualTag, 'value');
+               if not FLoadDict.trygetvalue(uppercase(TagAttributeName),TagAttrName) then
+                  TagAttrName:=siteattr_none;
+               if TagAttrName <> siteattr_none then begin
+                  case TagAttrName of
+                   siteattr_FRAMENAME       : FrameName:=TagAttributeValue;
+                   siteattr_WINDOWNAME      : WindowName:=TagAttributeValue;
+                   siteattr_WINDOW_STYLES   : WindowStyles:=StrToIntDef(TagAttributeValue,0);
+                   siteattr_EXWINDOW_STYLES : ExWindowStyles:=StrToIntDef(TagAttributeValue,0);
+                   siteattr_FONT            : Font:=TagAttributeValue;
+                   siteattr_IMAGELIST       : ImageList:=TagAttributeValue;
+                   siteattr_IMAGETYPE       : UseFolderImages:=uppercase(TagAttributeValue)='FOLDER';
+                   end;
              end;
              end;
-          end;
-       end;
-     end;
-  //end
+              // writeln('0:',flevel,' ' ,aactualtag,' ',tagname,' ' ,tagattributename, ' ' ,tagattributevalue);
+             end;
+             end;
+         end;
+      end;
+   end;
+// end; {body}
+  //end   {html}
 end;
 end;
 
 
 procedure TChmSiteMap.FoundText(AText: string);
 procedure TChmSiteMap.FoundText(AText: string);
@@ -342,14 +414,22 @@ destructor TChmSiteMap.Destroy;
 begin
 begin
   if Assigned(FHTMLParser) then FHTMLParser.Free;
   if Assigned(FHTMLParser) then FHTMLParser.Free;
   FItems.Free;
   FItems.Free;
+  FLoadDict.Free;
+
   Inherited Destroy;
   Inherited Destroy;
 end;
 end;
 
 
+procedure TChmSiteMap.Sort(Compare: TListSortCompare);
+begin
+  FItems.sort(compare);
+end;
+
 procedure TChmSiteMap.LoadFromFile(AFileName: String);
 procedure TChmSiteMap.LoadFromFile(AFileName: String);
 var
 var
   Buffer: String;
   Buffer: String;
   TmpStream: TMemoryStream;
   TmpStream: TMemoryStream;
 begin
 begin
+  CheckLookup;
   if Assigned(FHTMLParser) then FHTMLParser.Free;
   if Assigned(FHTMLParser) then FHTMLParser.Free;
   TmpStream := TMemoryStream.Create;
   TmpStream := TMemoryStream.Create;
   try
   try
@@ -362,8 +442,8 @@ begin
   end;
   end;
   FHTMLParser := THTMLParser.Create(Buffer);
   FHTMLParser := THTMLParser.Create(Buffer);
   try
   try
-    FHTMLParser.OnFoundTag := @FoundTag;
-    FHTMLParser.OnFoundText := @FoundText;
+    FHTMLParser.OnFoundTag := FoundTag;
+    FHTMLParser.OnFoundText := FoundText;
     FHTMLParser.Exec;
     FHTMLParser.Exec;
   finally
   finally
     FreeAndNil(FHTMLParser);
     FreeAndNil(FHTMLParser);
@@ -374,12 +454,13 @@ procedure TChmSiteMap.LoadFromStream(AStream: TStream);
 var
 var
   Buffer: String;
   Buffer: String;
 begin
 begin
+  CheckLookup;
   if Assigned(FHTMLParser) then FHTMLParser.Free;
   if Assigned(FHTMLParser) then FHTMLParser.Free;
   SetLength(Buffer, AStream.Size-AStream.Position);
   SetLength(Buffer, AStream.Size-AStream.Position);
   if AStream.Read(Buffer[1], AStream.Size-AStream.Position) > 0 then begin;
   if AStream.Read(Buffer[1], AStream.Size-AStream.Position) > 0 then begin;
     FHTMLParser := THTMLParser.Create(Buffer);
     FHTMLParser := THTMLParser.Create(Buffer);
-    FHTMLParser.OnFoundTag := @FoundTag;
-    FHTMLParser.OnFoundText := @FoundText;
+    FHTMLParser.OnFoundTag := FoundTag;
+    FHTMLParser.OnFoundText := FoundText;
     FHTMLParser.Exec;
     FHTMLParser.Exec;
     FreeAndNil(FHTMLParser);
     FreeAndNil(FHTMLParser);
   end;
   end;
@@ -397,6 +478,9 @@ begin
     end;
     end;
 end;
 end;
 
 
+// hhk items:  Merge | ([Name] ([Name] [Type...] [Local [URL] | See Also])... [FrameName] [WindowName] [Comment])
+// hhc items:  Merge | ([Name] ([Type...] [Local] [URL])... [FrameName] [WindowName] [Comment] [New] [ImageNumber])
+
 procedure TChmSiteMap.SaveToStream(AStream: TStream);
 procedure TChmSiteMap.SaveToStream(AStream: TStream);
 var
 var
   Indent: Integer;
   Indent: Integer;
@@ -408,44 +492,86 @@ var
      AStream.Write(AString[1], Length(AString));
      AStream.Write(AString[1], Length(AString));
      AStream.WriteByte(10);
      AStream.WriteByte(10);
   end;
   end;
+  procedure WriteStringNoIndent(AString: String);
+  var
+    I: Integer;
+  begin
+     AStream.Write(AString[1], Length(AString));
+  end;
+
   procedure WriteParam(AName: String; AValue: String);
   procedure WriteParam(AName: String; AValue: String);
   begin
   begin
     WriteString('<param name="'+AName+'" value="'+AValue+'">');
     WriteString('<param name="'+AName+'" value="'+AValue+'">');
   end;
   end;
   procedure WriteEntries(AItems: TChmSiteMapItems);
   procedure WriteEntries(AItems: TChmSiteMapItems);
   var
   var
-    I : Integer;
+    I,J : Integer;
     Item: TChmSiteMapItem;
     Item: TChmSiteMapItem;
+    Sub : TChmSiteMapSubItem;
+    lemitkeyword : boolean;
   begin
   begin
+    lemitkeyword:=ChmSiteMapGenerationOptions=emitkeyword;
     for I := 0 to AItems.Count-1 do begin
     for I := 0 to AItems.Count-1 do begin
       Item := AItems.Item[I];
       Item := AItems.Item[I];
+
+      {$ifdef preferlower}
+      WriteString('<li> <object type="text/sitemap">');
+      {$else}
       WriteString('<LI> <OBJECT type="text/sitemap">');
       WriteString('<LI> <OBJECT type="text/sitemap">');
+      {$endif}
       Inc(Indent, 8);
       Inc(Indent, 8);
 
 
-      if (SiteMapType = stIndex) and ((Item.Children.Count > 0) or (item.seealso<>'')) then
-         WriteParam('Keyword', Item.Text);
-      //if Item.KeyWord <> '' then WriteParam('Keyword', Item.KeyWord);
-      if Item.Text <> '' then WriteParam('Name', Item.Text);
-      if (Item.Local <> '') or (SiteMapType = stIndex) then WriteParam('Local', StringReplace(Item.Local, '\', '/', [rfReplaceAll]));
-      if Item.URL <> '' then WriteParam('URL', StringReplace(Item.URL, '\', '/', [rfReplaceAll]));
-      if (SiteMapType = stIndex) and (Item.SeeAlso <> '') then WriteParam('See Also', Item.SeeAlso);
-      //if Item.FrameName <> '' then WriteParam('FrameName', Item.FrameName);
-      //if Item.WindowName <> '' then WriteParam('WindowName', Item.WindowName);
+      if Item.Name<>'' then
+        begin
+          if lemitkeyword then
+            WriteParam('Keyword', item.Name)
+          else
+            WriteParam('Name', Item.Name);
+        end;
+
+      if item.FSubItems.count>0 then
+        begin
+          For j:=0 to item.FSubItems.count-1 do
+            begin
+              Sub:=TChmSiteMapSubItem(item.fsubitems[j]);
+              if Sub.Name <> ''     then WriteParam('Name', Sub.Name);
+              if Sub.ItemType <> '' then WriteParam('Type', Sub.ItemType);
+              if Sub.Local <> ''    then WriteParam('Local', Sub.Local);
+              if Sub.URL <> ''      then WriteParam('URL', Sub.URL);
+              if Sub.SeeAlso <> ''  then WriteParam('See Also', Sub.SeeAlso);
+            end;
+        end;
+      if Item.FrameName <> '' then WriteParam('FrameName', Item.FrameName);
+      if Item.WindowName <> '' then WriteParam('WindowName', Item.WindowName);
       if Item.Comment <> '' then WriteParam('Comment', Item.Comment);
       if Item.Comment <> '' then WriteParam('Comment', Item.Comment);
-      if (SiteMapType = stTOC) and (Item.IncreaseImageIndex) then WriteParam('New', 'yes'); // is this a correct value?
-      if (SiteMapType = stTOC) and (Item.ImageNumber <> -1) then WriteParam('ImageNumber', IntToStr(Item.ImageNumber));
-
+      if (SiteMapType = stTOC) and (Item.IncreaseImageIndex) then
+          WriteParam('New', 'yes'); // is this a correct value?
+      if (SiteMapType = stTOC) and (Item.ImageNumber <> -1) then
+          WriteParam('ImageNumber', IntToStr(Item.ImageNumber));
       Dec(Indent, 3);
       Dec(Indent, 3);
+      {$ifdef preferlower}
+      WriteString('</object>');
+      {$else}
       WriteString('</OBJECT>');
       WriteString('</OBJECT>');
+      {$endif}
       Dec(Indent, 5);
       Dec(Indent, 5);
 
 
       // Now Sub Entries
       // Now Sub Entries
       if Item.Children.Count > 0 then begin
       if Item.Children.Count > 0 then begin
-        WriteString('<UL>');
+        {$ifdef preferlower}
+        WriteString('<ul>');
+        {$else}
+        WriteString('<UL> ');
+        {$endif}
         Inc(Indent, 8);
         Inc(Indent, 8);
         WriteEntries(Item.Children);
         WriteEntries(Item.Children);
         Dec(Indent, 8);
         Dec(Indent, 8);
-        WriteString('</UL>');
+        {$ifdef preferlower}
+        WriteString('</ul>');
+        {$else}
+        WriteString('</UL>'); //writestringnoident
+        {$endif}
+
       end;
       end;
     end;
     end;
   end;
   end;
@@ -475,7 +601,7 @@ begin
     // both TOC and Index have font
     // both TOC and Index have font
     if Font <> '' then
     if Font <> '' then
       WriteParam('Font', Font);
       WriteParam('Font', Font);
-    Dec(Indent, 8);
+  Dec(Indent, 8);
   WriteString('</OBJECT>');
   WriteString('</OBJECT>');
   
   
   // And now the items
   // And now the items
@@ -501,19 +627,137 @@ begin
   FChildren := AValue;
   FChildren := AValue;
 end;
 end;
 
 
+function TChmSiteMapItem.getlocal: string;
+begin
+  result:='';
+  if FSubItems.count>0 then
+     result:=TChmSiteMapSubItem(FSubItems[0]).local;
+end;
+
+function TChmSiteMapItem.getseealso: string;
+begin
+  result:='';
+  if FSubItems.count>0 then
+    result:=TChmSiteMapSubItem(FSubItems[FSubItems.count-1]).SeeAlso;
+end;
+
+function TChmSiteMapItem.getsubitem( index : integer): TChmSiteMapSubItem;
+begin
+  result:=nil;
+  if index<FSubItems.count then
+    result:=TChmSiteMapSubItem(FSubItems[index]);
+end;
+
+function TChmSiteMapItem.getsubitemcount: integer;
+begin
+   result:=FSubItems.count;
+end;
+
 constructor TChmSiteMapItem.Create(AOwner: TChmSiteMapItems);
 constructor TChmSiteMapItem.Create(AOwner: TChmSiteMapItems);
 begin
 begin
   Inherited Create;
   Inherited Create;
   FOwner := AOwner;
   FOwner := AOwner;
   FChildren := TChmSiteMapItems.Create(Owner.Owner, Self);
   FChildren := TChmSiteMapItems.Create(Owner.Owner, Self);
+  FSubItems := TObjectList.Create(true);
+  imagenumber:=-1;
 end;
 end;
 
 
 destructor TChmSiteMapItem.Destroy;
 destructor TChmSiteMapItem.Destroy;
 begin
 begin
+  fsubitems.Free;
   FChildren.Free;
   FChildren.Free;
   Inherited Destroy;
   Inherited Destroy;
 end;
 end;
 
 
+procedure TChmSiteMapItem.AddName(const Name: string);
+var sub :TChmSiteMapSubItem;
+begin
+  if fname='' then
+    fname:=name
+  else
+    begin
+      sub :=TChmSiteMapSubItem.create(self);
+      FSubItems.add(sub);
+      sub.Name:=Name;
+    end;
+end;
+
+procedure TChmSiteMapItem.AddLocal(const Local: string);
+var sub :TChmSiteMapSubItem;
+    addnew : boolean;
+begin
+   if fsubitems.count>0 then
+      begin
+        sub:=TChmSiteMapSubItem(fsubitems[FSubItems.count-1]);
+        if sub.FLocal<>'' then
+          begin
+            sub.flocal:=local;
+            exit;
+          end;
+      end;
+   sub :=TChmSiteMapSubItem.create(self);
+   FSubItems.add(sub);
+//   sub.name:=name;
+   sub.Local:=Local;
+end;
+
+procedure TChmSiteMapItem.AddSeeAlso(const SeeAlso: string);
+// see also is mutually exclusive with "local url", so addition procedure is same as "local"
+var sub :TChmSiteMapSubItem;
+begin
+   if fsubitems.count>0 then
+      begin
+        sub:=TChmSiteMapSubItem(fsubitems[FSubItems.count-1]);
+        if sub.FSeeAlso<>'' then
+          begin
+            sub.FSeeAlso:=SeeAlso;
+            exit;
+          end;
+      end;
+   sub :=TChmSiteMapSubItem.create(self);
+   FSubItems.add(sub);
+   sub.FSeeAlso:=SeeAlso;
+end;
+
+
+procedure TChmSiteMapItem.AddURL(const URL: string);
+var sub :TChmSiteMapSubItem;
+begin
+   if fsubitems.count>0 then
+      begin
+        sub:=TChmSiteMapSubItem(fsubitems[FSubItems.count-1]);
+        if sub.FURL<>'' then
+          begin
+            sub.fURL:=URL;
+            exit;
+          end;
+      end
+   { else not possible according to chmspec. An URL must always follow a "local" item}
+end;
+
+procedure TChmSiteMapItem.AddType(const AType: string);
+// in Tocs, Type can be the first is the same as local
+var sub :TChmSiteMapSubItem;
+begin
+   if fsubitems.count>0 then
+      begin
+        sub:=TChmSiteMapSubItem(fsubitems[FSubItems.count-1]);
+        if sub.ItemType<>'' then
+          begin
+            sub.ItemType:=AType;
+            exit;
+          end;
+      end;
+   sub :=TChmSiteMapSubItem.create(self);
+   FSubItems.add(sub);
+   sub.ItemType:=AType;
+end;
+
+procedure TChmSiteMapItem.Sort(Compare: TListSortCompare);
+begin
+  FChildren.sort(compare);
+end;
+
 { TChmSiteMapItems }
 { TChmSiteMapItems }
 
 
 function TChmSiteMapItems.GetItem(AIndex: Integer): TChmSiteMapItem;
 function TChmSiteMapItems.GetItem(AIndex: Integer): TChmSiteMapItem;
@@ -521,6 +765,15 @@ begin
   Result := TChmSiteMapItem(FList.Items[AIndex]);
   Result := TChmSiteMapItem(FList.Items[AIndex]);
 end;
 end;
 
 
+function TChmSiteMapItems.getparentname: String;
+begin
+  result:='Not assigned';
+  if assigned(fparentitem) then
+    begin
+      result:=FParentItem.name;
+    end;
+end;
+
 function TChmSiteMapItems.GetCount: Integer;
 function TChmSiteMapItems.GetCount: Integer;
 begin
 begin
   Result := FList.Count;
   Result := FList.Count;
@@ -577,8 +830,11 @@ begin
 end;
 end;
 
 
 procedure TChmSiteMapItems.Sort(Compare: TListSortCompare);
 procedure TChmSiteMapItems.Sort(Compare: TListSortCompare);
+var I :Integer;
 begin
 begin
   FList.Sort(Compare);
   FList.Sort(Compare);
+  for i:=0 to flist.Count-1 do
+    TChmSiteMapItem(flist[i]).sort(Compare)
 end;
 end;
 
 
 end.
 end.

+ 103 - 35
packages/chm/src/chmwriter.pas

@@ -6,7 +6,7 @@
   option) any later version.
   option) any later version.
 
 
   This program is distributed in the hope that it will be useful, but WITHOUT
   This program is distributed in the hope that it will be useful, but WITHOUT
-  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
   FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
   FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
   for more details.
   for more details.
 
 
@@ -22,7 +22,7 @@ unit chmwriter;
 {$MODE OBJFPC}{$H+}
 {$MODE OBJFPC}{$H+}
 
 
 interface
 interface
-uses Classes, ChmBase, chmtypes, chmspecialfiles, HtmlIndexer, chmsitemap, contnrs, StreamEx, Avl_Tree, lzxcompressthread;
+uses Generics.Collections,Classes, ChmBase, chmtypes, chmspecialfiles, HtmlIndexer, chmsitemap, contnrs, StreamEx, Avl_Tree, lzxcompressthread;
 
 
 Const
 Const
    DefaultHHC = 'Default.hhc';
    DefaultHHC = 'Default.hhc';
@@ -154,6 +154,7 @@ Type
     FAvlStrings   : TAVLTree;    // dedupe strings
     FAvlStrings   : TAVLTree;    // dedupe strings
     FAVLTopicdedupe : TAVlTree;  // Topic deduping, if we load it both from hhp and TOC
     FAVLTopicdedupe : TAVlTree;  // Topic deduping, if we load it both from hhp and TOC
     FAvlURLStr    : TAVLTree;    // dedupe urltbl + binindex must resolve URL to topicid
     FAvlURLStr    : TAVLTree;    // dedupe urltbl + binindex must resolve URL to topicid
+    FDictTopicsUrlInd    : specialize TDictionary<string,integer>; // if url exists reuse topic.
     SpareString   : TStringIndex;
     SpareString   : TStringIndex;
     SpareUrlStr   : TUrlStrIndex;
     SpareUrlStr   : TUrlStrIndex;
     FWindows      : TObjectList;
     FWindows      : TObjectList;
@@ -186,6 +187,7 @@ Type
     function AddURL(AURL: String; TopicsIndex: DWord): LongWord;
     function AddURL(AURL: String; TopicsIndex: DWord): LongWord;
     procedure CheckFileMakeSearchable(AStream: TStream; AFileEntry: TFileEntryRec);
     procedure CheckFileMakeSearchable(AStream: TStream; AFileEntry: TFileEntryRec);
     function AddTopic(ATitle,AnUrl:AnsiString;code:integer=-1):integer;
     function AddTopic(ATitle,AnUrl:AnsiString;code:integer=-1):integer;
+    function AddTopicindex(ATitle,AnUrl:AnsiString;code:integer=-1):integer;
     procedure ScanSitemap(asitemap:TCHMSiteMap);
     procedure ScanSitemap(asitemap:TCHMSiteMap);
     function NextTopicIndex: Integer;
     function NextTopicIndex: Integer;
     procedure Setwindows (AWindowList:TObjectList);
     procedure Setwindows (AWindowList:TObjectList);
@@ -1521,6 +1523,7 @@ begin
   FDefaultWindow:= '';
   FDefaultWindow:= '';
   FMergeFiles   :=TStringList.Create;
   FMergeFiles   :=TStringList.Create;
   FNrTopics     :=0;
   FNrTopics     :=0;
+  FDictTopicsUrlInd    :=specialize TDictionary<string,integer>.Create;
 end;
 end;
 
 
 destructor TChmWriter.Destroy;
 destructor TChmWriter.Destroy;
@@ -1543,7 +1546,7 @@ begin
   FAVLTopicdedupe.FreeAndClear;
   FAVLTopicdedupe.FreeAndClear;
   FAVLTopicdedupe.free;
   FAVLTopicdedupe.free;
   FWindows.Free;
   FWindows.Free;
-
+  FDictTopicsUrlInd.Free;
   inherited Destroy;
   inherited Destroy;
 end;
 end;
 
 
@@ -1664,6 +1667,7 @@ var
     TopicEntry: TTopicEntry;
     TopicEntry: TTopicEntry;
 
 
 begin
 begin
+    ATitle :=StringReplace(Atitle, '&x27;', '', [rfReplaceAll]);
     anurl:=StringReplace(anurl, '\', '/', [rfReplaceAll]);
     anurl:=StringReplace(anurl, '\', '/', [rfReplaceAll]);
     if ATitle <> '' then
     if ATitle <> '' then
       TopicEntry.StringsOffset := AddString(ATitle)
       TopicEntry.StringsOffset := AddString(ATitle)
@@ -1691,6 +1695,35 @@ begin
     FTopicsStream.WriteDWord(LEtoN(TopicEntry.URLTableOffset));
     FTopicsStream.WriteDWord(LEtoN(TopicEntry.URLTableOffset));
     FTopicsStream.WriteWord(LEtoN(TopicEntry.InContents));
     FTopicsStream.WriteWord(LEtoN(TopicEntry.InContents));
     FTopicsStream.WriteWord(LEtoN(TopicEntry.Unknown));
     FTopicsStream.WriteWord(LEtoN(TopicEntry.Unknown));
+    {$ifdef binindex}
+    writeln('topout:',result, ' ' , TopicEntry.StringsOffset,' ' ,TopicEntry.URLTableOffset, ' ',atitle,' - ', anurl);
+    {$endif}
+end;
+
+function TChmWriter.AddTopicindex(ATitle, AnUrl: AnsiString; code: integer
+  ): integer;
+
+begin
+   ATitle :=StringReplace(Atitle, '&x27;', '', [rfReplaceAll]);
+
+  // adhoc subsitutions. Replace with real code if exact behaviour is known.
+{  Atitle:=StringReplace(atitle, '&x27;', '''', [rfReplaceAll]);
+  if length(atitle)>0 then
+    atitle[1]:=uppercase(atitle[1])[1];}
+  {$ifdef binindex}
+  writeln('Enter ',ATitle,' ',AnUrl);
+  {$endif}
+  if FDictTopicsUrlInd.trygetvalue(anurl,result) then
+   begin
+     {$ifdef binindex}
+       writeln('found:',result);
+     {$endif}
+   end
+   else
+    begin
+      result:=addtopic(atitle,anurl);
+      FDictTopicsUrlInd.add(anurl,result);
+    end;
 end;
 end;
 
 
 procedure TChmWriter.ScanSitemap(asitemap:TCHMSiteMap);
 procedure TChmWriter.ScanSitemap(asitemap:TCHMSiteMap);
@@ -2039,32 +2072,64 @@ begin
   inc(blockind,indexentrysize);
   inc(blockind,indexentrysize);
 end;
 end;
 
 
-procedure CreateEntry(Item:TChmSiteMapItem;Str:WideString;commaatposition:integer);
+procedure WritestrNT(var p:pbyte;const str:Unicodestring);
+var i : integer;
+    p2 : pbyte;
+begin
+  p2:=p;
+  for i:=1 to Length(str) do
+    WriteWord(p2,Word(str[i]));   // write the wstr in little endian
+  WriteWord(p2,0);                // NT
+  p:=p2;
+end;
+
+procedure CreateEntry(Item:TChmSiteMapItem;const Str:UnicodeString;commaatposition,level:integer);
 
 
 var p      : pbyte;
 var p      : pbyte;
     topicid: integer;
     topicid: integer;
     seealso: Integer;
     seealso: Integer;
     entrysize:Integer;
     entrysize:Integer;
     i      : Integer;
     i      : Integer;
+    sb :TChmSiteMapSubItem;
 begin
 begin
   inc(TotalEntries);
   inc(TotalEntries);
   fillchar(testblock[0],DefBlockSize,#0);
   fillchar(testblock[0],DefBlockSize,#0);
   p:=@TestBlock[0];
   p:=@TestBlock[0];
-  for i:=1 to Length(str) do
-    WriteWord(p,Word(str[i]));   // write the wstr in little endian
-  WriteWord(p,0);                // NT
-//  if item.seealso='' then    // no seealso for now
-    seealso:=0;
- // else
-//    seealso:=2;
+
+  WritestrNT(p,Str);
+  if item.seealso='' then    // no seealso for now
+    seealso:=0
+   else
+    seealso:=2;
   WriteWord(p,seealso);          // =0 not a see also 2 =seealso
   WriteWord(p,seealso);          // =0 not a see also 2 =seealso
-  WriteWord(p,0);                // Entrydepth.  We can't know it, so write 2.
+  WriteWord(p,level);            // Entrydepth.  We can't know it, so write 2.
   WriteDword(p,commaatposition); // position of the comma
   WriteDword(p,commaatposition); // position of the comma
   WriteDword(p,0);               // unused 0
   WriteDword(p,0);               // unused 0
-  WriteDword(p,1);               // for now only local pair.
-  TopicId:=AddTopic(Item.Text,item.Local);
-  WriteDword(p,TopicId);
-  // if seealso then _here_ a wchar NT string with seealso?
+
+  if seealso=2 then
+   begin
+     {$ifdef binindex}
+     write('!seealso');
+     {$endif}
+     WriteDword(p,1);
+     WritestrNT(p,item.seealso)
+   end
+  else
+    begin
+      WriteDword(p,item.SubItemcount);
+      for i:=0 to item.SubItemcount-1 do
+        begin
+          sb:=item.SubItem[i];
+          if sb.name='' then
+            sb.name:=item.name;
+          {$ifdef binindex}
+          writeln('---',sb.name,' ',sb.local);
+          {$endif}
+          TopicId:=AddTopicIndex(sb.Name,sb.Local);
+          WriteDword(p,TopicId);
+        end;
+    end;
+
   WriteDword(p,1);               // always 1 (unknown);
   WriteDword(p,1);               // always 1 (unknown);
   WriteDword(p,mod13value);      //a value that increments with 13.
   WriteDword(p,mod13value);      //a value that increments with 13.
   mod13value:=mod13value+13;
   mod13value:=mod13value+13;
@@ -2158,32 +2223,36 @@ begin
   Result:=blk-start;
   Result:=blk-start;
 end;
 end;
 
 
-procedure CombineWithChildren(ParentItem:TChmSiteMapItem;Str:WideString;commaatposition:integer;first:boolean);
+procedure CombineWithChildren(ParentItem:TChmSiteMapItem;Str:UnicodeString;commaatposition:integer;level:integer);
 var i    : Integer;
 var i    : Integer;
-    Item : TChmSiteMapItem;
-begin
-  if ParentItem.Children.Count = 0 Then
-    Begin
+    llItem : TChmSiteMapItem;
+begin
+   str:=StringReplace(str, '&x27;', '', [rfReplaceAll]);
+   {$ifdef binindex}
+     writeln('i:',level,' ',str);
+   {$endif}
+//  if ParentItem.Children.Count = 0 Then
+//    Begin
      // comment/fix next
      // comment/fix next
      //   if commatposition=length(str) then commaatposition:=0;
      //   if commatposition=length(str) then commaatposition:=0;
-       if first then
-        CreateEntry(ParentItem,Str,0)
+       if level=0 then
+        CreateEntry(ParentItem,Str,0,level)
        else
        else
-        CreateEntry(ParentItem,Str,commaatposition);
-    End
-  Else
+        CreateEntry(ParentItem,Str,commaatposition,level);
+//    End
+//  Else
     for i:=0 to ParentItem.Children.Count-1 do
     for i:=0 to ParentItem.Children.Count-1 do
       begin
       begin
-        item := TChmSiteMapItem(ParentItem.Children.Item[i]);
-        if first Then
-          CombineWithChildren(Item,Str+', '+item.text,commaatposition+2,false)
-        else
-          CombineWithChildren(Item,Str+', '+item.text,commaatposition,false);
+        llitem := TChmSiteMapItem(ParentItem.Children.Item[i]);
+{        if level=0 Then
+          CombineWithChildren(Item,str+', '+item.text,0,level+1)
+        else}
+          CombineWithChildren(llItem,Str+', '+llitem.text,length(str)+2,level+1);
       end;
       end;
 end;
 end;
 
 
 Var i             : Integer;
 Var i             : Integer;
-    Key           : WideString;
+    Key           : UnicodeString;
     Item          : TChmSiteMapItem;
     Item          : TChmSiteMapItem;
     ListingBlocks : Integer;
     ListingBlocks : Integer;
     EntryBytes    : Integer;
     EntryBytes    : Integer;
@@ -2204,6 +2273,7 @@ begin
   {$ifdef binindex}
   {$ifdef binindex}
     writeln('starting index');
     writeln('starting index');
   {$endif}
   {$endif}
+  ASiteMap.sort(@indexitemcompare);
   IndexStream:=TMemoryStream.Create;
   IndexStream:=TMemoryStream.Create;
   indexstream.size:=sizeof(TBTreeHeader);
   indexstream.size:=sizeof(TBTreeHeader);
   IndexStream.position:=Sizeof(TBTreeHeader);
   IndexStream.position:=Sizeof(TBTreeHeader);
@@ -2251,7 +2321,7 @@ begin
       // so we can see if Windows loads the binary or textual index.
       // so we can see if Windows loads the binary or textual index.
       CombineWithChildren(Item,Key+'2',length(key)+1,true);
       CombineWithChildren(Item,Key+'2',length(key)+1,true);
       {$else}
       {$else}
-      CombineWithChildren(Item,Key,length(key),true);
+      CombineWithChildren(Item,Key,length(key),0);
       {$endif}
       {$endif}
     end;
     end;
   PrepareCurrentBlock(True);     // flush last listing block.
   PrepareCurrentBlock(True);     // flush last listing block.
@@ -2420,7 +2490,6 @@ begin
   PostAddStreamToArchive(AName, '/', AStream);
   PostAddStreamToArchive(AName, '/', AStream);
 end;
 end;
 
 
-
 procedure TChmWriter.AddContext(AContext: DWord; ATopic: String);
 procedure TChmWriter.AddContext(AContext: DWord; ATopic: String);
 var
 var
   Offset: DWord;
   Offset: DWord;
@@ -2448,7 +2517,6 @@ begin
 end;
 end;
 
 
 procedure TChmWriter.Setwindows(AWindowList: TObjectList);
 procedure TChmWriter.Setwindows(AWindowList: TObjectList);
-
 var i : integer;
 var i : integer;
     x : TCHMWindow;
     x : TCHMWindow;
 begin
 begin

+ 16 - 16
utils/fpdoc/dw_htmlchm.inc

@@ -192,12 +192,12 @@ begin
         // by unit
         // by unit
         TmpItem := ObjUnitItem.Children.NewItem;
         TmpItem := ObjUnitItem.Children.NewItem;
         TmpItem.Text := Element.Name;
         TmpItem.Text := Element.Name;
-        TmpItem.Local := FixHTMLpath(Allocator.GetFilename(Element, 0));
+        TmpItem.addLocal(FixHTMLpath(Allocator.GetFilename(Element, 0)));
         
         
         //alpha
         //alpha
         TmpItem := GetAlphaItem(AlphaObjItem.Children, UpperCase(Copy(Element.Name, 1, 2))).Children.NewItem;
         TmpItem := GetAlphaItem(AlphaObjItem.Children, UpperCase(Copy(Element.Name, 1, 2))).Children.NewItem;
         TmpItem.Text := Element.Name;
         TmpItem.Text := Element.Name;
-        TmpItem.Local := FixHTMLpath(Allocator.GetFilename(Element, 0));
+        TmpItem.addLocal(FixHTMLpath(Allocator.GetFilename(Element, 0)));
         
         
       end;
       end;
       
       
@@ -208,12 +208,12 @@ begin
         // by unit
         // by unit
         TmpItem := RoutinesUnitItem.Children.NewItem;
         TmpItem := RoutinesUnitItem.Children.NewItem;
         TmpItem.Text := Element.Name;
         TmpItem.Text := Element.Name;
-        TmpItem.Local := FixHTMLpath(Allocator.GetFilename(Element, 0));
+        TmpItem.addLocal(FixHTMLpath(Allocator.GetFilename(Element, 0)));
         
         
         // alpha
         // alpha
         TmpItem := GetAlphaItem(AlphaRoutinesItem.Children, UpperCase(Element.Name[1])).Children.NewItem;
         TmpItem := GetAlphaItem(AlphaRoutinesItem.Children, UpperCase(Element.Name[1])).Children.NewItem;
         TmpItem.Text := Element.Name;
         TmpItem.Text := Element.Name;
-        TmpItem.Local := FixHTMLpath(Allocator.GetFilename(Element, 0));
+        TmpItem.addLocal(FixHTMLpath(Allocator.GetFilename(Element, 0)));
       end;
       end;
     end;
     end;
   end;
   end;
@@ -305,7 +305,7 @@ begin
         continue;
         continue;
       ParentItem := Index.Items.NewItem;
       ParentItem := Index.Items.NewItem;
       ParentItem.Text := AModule.Name;
       ParentItem.Text := AModule.Name;
-      ParentItem.Local := FixHTMLpath(Allocator.GetFilename(AModule, 0));
+      ParentItem.addLocal(FixHTMLpath(Allocator.GetFilename(AModule, 0)));
 
 
       //  classes
       //  classes
       for j := 0 to AModule.InterfaceSection.Classes.Count-1 do
       for j := 0 to AModule.InterfaceSection.Classes.Count-1 do
@@ -313,7 +313,7 @@ begin
         ParentElement := TPasClassType(AModule.InterfaceSection.Classes[j]);
         ParentElement := TPasClassType(AModule.InterfaceSection.Classes[j]);
         ParentItem := Index.Items.NewItem;
         ParentItem := Index.Items.NewItem;
         ParentItem.Text := ParentELement.Name;
         ParentItem.Text := ParentELement.Name;
-        ParentItem.Local := FixHTMLpath(Allocator.GetFilename(ParentElement, 0));
+        ParentItem.addLocal(FixHTMLpath(Allocator.GetFilename(ParentElement, 0)));
         for k := 0 to TPasClassType(ParentElement).Members.Count-1 do
         for k := 0 to TPasClassType(ParentElement).Members.Count-1 do
         begin
         begin
           TmpElement := TPasElement(TPasClassType(ParentElement).Members.Items[k]);
           TmpElement := TPasElement(TPasClassType(ParentElement).Members.Items[k]);
@@ -336,11 +336,11 @@ begin
             cmtInterface   : TmpItem.Text := TmpElement.Name + ' interface';
             cmtInterface   : TmpItem.Text := TmpElement.Name + ' interface';
             cmtUnknown     : TmpItem.Text := TmpElement.Name;
             cmtUnknown     : TmpItem.Text := TmpElement.Name;
           end;
           end;
-          TmpItem.Local := FixHTMLpath(Allocator.GetFilename(TmpElement, 0));
+          TmpItem.addLocal(FixHTMLpath(Allocator.GetFilename(TmpElement, 0)));
           if (trim(s)<>'') and (tmpitem.local<>s) then
           if (trim(s)<>'') and (tmpitem.local<>s) then
             begin
             begin
               writeln('Hint: Index: Resolved:',tmpitem.local,' to ',s);
               writeln('Hint: Index: Resolved:',tmpitem.local,' to ',s);
-              tmpitem.local:=s;
+              tmpitem.addLocal(s);
             end;
             end;
 
 
           {
           {
@@ -350,11 +350,11 @@ begin
           MemberItem := nil;
           MemberItem := nil;
           MemberItem := GetAlphaItem(Index.Items, TmpElement.Name);
           MemberItem := GetAlphaItem(Index.Items, TmpElement.Name);
           // ahh! if MemberItem.Local is empty MemberType is not shown!
           // ahh! if MemberItem.Local is empty MemberType is not shown!
-          MemberItem.Local := FixHTMLpath(Allocator.GetFilename(TmpElement, 0));
+          MemberItem.addLocal(FixHTMLpath(Allocator.GetFilename(TmpElement, 0)));
 
 
           TmpItem := MemberItem.Children.NewItem;
           TmpItem := MemberItem.Children.NewItem;
           TmpItem.Text := ParentElement.Name;
           TmpItem.Text := ParentElement.Name;
-          TmpITem.Local := FixHTMLpath(Allocator.GetFilename(TmpElement, 0));
+          TmpITem.addLocal(FixHTMLpath(Allocator.GetFilename(TmpElement, 0)));
         end;
         end;
       end;
       end;
       // routines
       // routines
@@ -363,7 +363,7 @@ begin
         ParentElement := TPasProcedureType(AModule.InterfaceSection.Functions[j]);
         ParentElement := TPasProcedureType(AModule.InterfaceSection.Functions[j]);
         TmpItem := Index.Items.NewItem;
         TmpItem := Index.Items.NewItem;
         TmpItem.Text := ParentElement.Name + ' ' + ParentElement.ElementTypeName;
         TmpItem.Text := ParentElement.Name + ' ' + ParentElement.ElementTypeName;
-        TmpItem.Local := FixHTMLpath(Allocator.GetFilename(ParentElement, 0));
+        TmpItem.addLocal(FixHTMLpath(Allocator.GetFilename(ParentElement, 0)));
       end;
       end;
       // consts
       // consts
       for j := 0 to AModule.InterfaceSection.Consts.Count-1 do
       for j := 0 to AModule.InterfaceSection.Consts.Count-1 do
@@ -371,7 +371,7 @@ begin
         ParentElement := TPasElement(AModule.InterfaceSection.Consts[j]);
         ParentElement := TPasElement(AModule.InterfaceSection.Consts[j]);
         TmpItem := Index.Items.NewItem;
         TmpItem := Index.Items.NewItem;
         TmpItem.Text := ParentElement.Name;
         TmpItem.Text := ParentElement.Name;
-        TmpItem.Local := FixHTMLpath(Allocator.GetFilename(ParentElement, 0));
+        TmpItem.addLocal(FixHTMLpath(Allocator.GetFilename(ParentElement, 0)));
       end;
       end;
       // types
       // types
       for j := 0 to AModule.InterfaceSection.Types.Count-1 do
       for j := 0 to AModule.InterfaceSection.Types.Count-1 do
@@ -379,7 +379,7 @@ begin
         ParentElement := TPasType(AModule.InterfaceSection.Types[j]);
         ParentElement := TPasType(AModule.InterfaceSection.Types[j]);
         TmpItem := Index.Items.NewItem;
         TmpItem := Index.Items.NewItem;
         TmpItem.Text := ParentElement.Name;
         TmpItem.Text := ParentElement.Name;
-        TmpItem.Local := FixHTMLpath(Allocator.GetFilename(ParentElement, 0));
+        TmpItem.addLocal(FixHTMLpath(Allocator.GetFilename(ParentElement, 0)));
         // enums
         // enums
         if ParentELement is TPasEnumType then
         if ParentELement is TPasEnumType then
         begin
         begin
@@ -390,11 +390,11 @@ begin
             // subitem
             // subitem
             TmpItem := ParentItem.Children.NewItem;
             TmpItem := ParentItem.Children.NewItem;
             TmpItem.Text := TmpElement.Name;
             TmpItem.Text := TmpElement.Name;
-            TmpItem.Local := ParentItem.Local;
+            TmpItem.addLocal(ParentItem.Local);
             // root level
             // root level
             TmpItem := Index.Items.NewItem;
             TmpItem := Index.Items.NewItem;
             TmpItem.Text := TmpElement.Name;
             TmpItem.Text := TmpElement.Name;
-            TmpItem.Local := ParentItem.Local;
+            TmpItem.addLocal(ParentItem.Local);
           end;
           end;
         end;
         end;
       end;
       end;
@@ -404,7 +404,7 @@ begin
         ParentElement := TPasElement(AModule.InterfaceSection.Variables[j]);
         ParentElement := TPasElement(AModule.InterfaceSection.Variables[j]);
         TmpItem := Index.Items.NewItem;
         TmpItem := Index.Items.NewItem;
         TmpItem.Text := ParentElement.Name + ' var';
         TmpItem.Text := ParentElement.Name + ' var';
-        TmpItem.Local := FixHTMLpath(Allocator.GetFilename(ParentElement, 0));
+        TmpItem.addLocal(FixHTMLpath(Allocator.GetFilename(ParentElement, 0)));
       end;
       end;
       // declarations
       // declarations
       {
       {