ISHelpGen.dpr 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032
  1. program ISHelpGen;
  2. {$APPTYPE CONSOLE}
  3. uses
  4. Windows,
  5. SysUtils,
  6. StrUtils,
  7. Classes,
  8. ActiveX,
  9. ComObj,
  10. TypInfo,
  11. XMLParse in 'XMLParse.pas',
  12. UIsxclassesParser in 'UIsxclassesParser.pas',
  13. PathFunc in '..\..\Components\PathFunc.pas';
  14. const
  15. Version = '1.21';
  16. XMLFileVersion = '1';
  17. SNewLine = #13#10;
  18. type
  19. TElement = (
  20. el_Text,
  21. elA,
  22. elAnchorLink,
  23. elB,
  24. elBody,
  25. elBR,
  26. elContents,
  27. elContentsHeading,
  28. elContentsTopic,
  29. elDD,
  30. elDL,
  31. elDT,
  32. elExample,
  33. elExamples,
  34. elExtLink,
  35. elFlag,
  36. elFlagList,
  37. elHeading,
  38. elI,
  39. elIndent,
  40. elKeyword,
  41. elLI,
  42. elLink,
  43. elOL,
  44. elP,
  45. elParam,
  46. elParamList,
  47. elPre,
  48. elPreCode,
  49. elSetupDefault,
  50. elSetupFormat,
  51. elSetupValid,
  52. elSetupTopic,
  53. elSmall,
  54. elTable,
  55. elTD,
  56. elTopic,
  57. elTR,
  58. elTT,
  59. elU,
  60. elUL);
  61. TElementSet = set of TElement;
  62. TKeywordInfo = class
  63. public
  64. Topic, Anchor: String;
  65. end;
  66. var
  67. SourceDir, OutputDir: String;
  68. NoContentsHtm: Boolean;
  69. Keywords, DefinedTopics, TargetTopics, SetupDirectives: TStringList;
  70. TopicsGenerated: Integer = 0;
  71. CurrentTopicName: String;
  72. CurrentListIsCompact: Boolean;
  73. CurrentTableColumnIndex: Integer;
  74. procedure UnexpectedElementError(const Node: IXMLNode);
  75. begin
  76. raise Exception.CreateFmt('Element "%s" is unexpected here', [Node.NodeName]);
  77. end;
  78. function ElementFromNode(const Node: IXMLNode): TElement;
  79. var
  80. I: Integer;
  81. begin
  82. case Node.NodeType of
  83. NODE_ELEMENT:
  84. begin
  85. I := GetEnumValue(TypeInfo(TElement), 'el' + Node.NodeName);
  86. if I < 0 then
  87. raise Exception.CreateFmt('Unknown element "%s"', [Node.NodeName]);
  88. Result := TElement(I);
  89. end;
  90. NODE_TEXT, NODE_ENTITY_REFERENCE: Result := el_Text;
  91. else
  92. raise Exception.CreateFmt('ElementFromNode: Unknown node type %d', [Node.NodeType]);
  93. end;
  94. end;
  95. function IsWhitespace(const Node: IXMLNode): Boolean;
  96. { Returns True if the node is text that consists only of whitespace }
  97. var
  98. S: String;
  99. I: Integer;
  100. begin
  101. Result := False;
  102. if Node.NodeType = NODE_TEXT then begin
  103. S := Node.Text;
  104. for I := 1 to Length(S) do
  105. if not CharInSet(S[I], [#9, #10, ' ']) then
  106. Exit;
  107. Result := True;
  108. end;
  109. end;
  110. function IsFirstNonWhitespaceNode(Node: IXMLNode): Boolean;
  111. { Returns True if there are no preceding non-whitespace sibling elements }
  112. begin
  113. repeat
  114. Node := Node.PreviousSibling;
  115. until (Node = nil) or not IsWhitespace(Node);
  116. Result := (Node = nil);
  117. end;
  118. function IsLastNonWhitespaceNode(Node: IXMLNode): Boolean;
  119. { Returns True if no non-whitespace sibling elements follow }
  120. begin
  121. repeat
  122. Node := Node.NextSibling;
  123. until (Node = nil) or not IsWhitespace(Node);
  124. Result := (Node = nil);
  125. end;
  126. function NodeHasChildren(Node: IXMLNode): Boolean;
  127. { Returns True if the node has non-whitespace children }
  128. begin
  129. Node := Node.GetFirstChild;
  130. while Assigned(Node) do begin
  131. if not IsWhitespace(Node) then begin
  132. Result := True;
  133. Exit;
  134. end;
  135. Node := Node.NextSibling;
  136. end;
  137. Result := False;
  138. end;
  139. function ListItemExists(const SL: TStrings; const S: String): Boolean;
  140. var
  141. I: Integer;
  142. begin
  143. for I := 0 to SL.Count-1 do
  144. if SL[I] = S then begin
  145. Result := True;
  146. Exit;
  147. end;
  148. Result := False;
  149. end;
  150. function StringChange(var S: String; const FromStr, ToStr: String): Integer;
  151. var
  152. FromStrLen, I, EndPos, J: Integer;
  153. IsMatch: Boolean;
  154. label 1;
  155. begin
  156. Result := 0;
  157. if FromStr = '' then Exit;
  158. FromStrLen := Length(FromStr);
  159. I := 1;
  160. 1:EndPos := Length(S) - FromStrLen + 1;
  161. while I <= EndPos do begin
  162. IsMatch := True;
  163. J := 0;
  164. while J < FromStrLen do begin
  165. if S[J+I] <> FromStr[J+1] then begin
  166. IsMatch := False;
  167. Break;
  168. end;
  169. Inc(J);
  170. end;
  171. if IsMatch then begin
  172. Inc(Result);
  173. Delete(S, I, FromStrLen);
  174. Insert(ToStr, S, I);
  175. Inc(I, Length(ToStr));
  176. goto 1;
  177. end;
  178. Inc(I);
  179. end;
  180. end;
  181. procedure SaveStringToFile(const S, Filename: String);
  182. var
  183. F: TFileStream;
  184. U: UTF8String;
  185. begin
  186. F := TFileStream.Create(Filename, fmCreate);
  187. try
  188. U := UTF8String(S);
  189. F.WriteBuffer(U[1], Length(U));
  190. finally
  191. F.Free;
  192. end;
  193. end;
  194. function EscapeHTML(const S: String; const EscapeDoubleQuotes: Boolean = True): String;
  195. begin
  196. Result := S;
  197. StringChange(Result, '&', '&amp;');
  198. StringChange(Result, '<', '&lt;');
  199. StringChange(Result, '>', '&gt;');
  200. if EscapeDoubleQuotes then
  201. StringChange(Result, '"', '&quot;');
  202. { Also convert the Unicode representation of a non-breaking space into &nbsp;
  203. so it's easily to tell them apart from normal spaces when viewing the
  204. generated HTML source }
  205. StringChange(Result, #$00A0, '&nbsp;');
  206. end;
  207. procedure CheckTopicNameValidity(const TopicName: String);
  208. var
  209. I: Integer;
  210. begin
  211. if TopicName = '' then
  212. raise Exception.Create('Topic name cannot be empty');
  213. { Security: Make sure topic names don't include slashes etc. }
  214. for I := 1 to Length(TopicName) do
  215. if not CharInSet(TopicName[I], ['A'..'Z', 'a'..'z', '0'..'9', '_', '-']) then
  216. raise Exception.CreateFmt('Topic name "%s" includes invalid characters', [TopicName]);
  217. end;
  218. procedure CheckAnchorNameValidity(const AnchorName: String);
  219. var
  220. I: Integer;
  221. begin
  222. if AnchorName = '' then
  223. raise Exception.Create('Anchor name cannot be empty');
  224. for I := 1 to Length(AnchorName) do
  225. if not CharInSet(AnchorName[I], ['A'..'Z', 'a'..'z', '0'..'9', '_', '-', '.']) then
  226. raise Exception.CreateFmt('Anchor name "%s" includes invalid characters', [AnchorName]);
  227. end;
  228. function GenerateTopicFilename(const TopicName: String): String;
  229. begin
  230. CheckTopicNameValidity(TopicName);
  231. Result := 'topic_' + Lowercase(TopicName) + '.htm';
  232. end;
  233. function GenerateTopicLink(const TopicName, AnchorName: String): String;
  234. begin
  235. if TopicName <> '' then
  236. Result := GenerateTopicFileName(TopicName)
  237. else begin
  238. Result := '';
  239. if AnchorName = '' then
  240. raise Exception.Create('Cannot create link with neither a target topic nor anchor');
  241. end;
  242. if AnchorName <> '' then begin
  243. CheckAnchorNameValidity(AnchorName);
  244. Result := Result + '#' + AnchorName;
  245. end;
  246. end;
  247. function GenerateAnchorHTML(const AnchorName, InnerContents: String): String;
  248. { Generates HTML for an anchor on the current topic, also updating
  249. DefinedTopics and checking for duplicates }
  250. var
  251. S: String;
  252. begin
  253. if CurrentTopicName = '' then
  254. raise Exception.Create('Cannot create anchor outside of topic');
  255. CheckAnchorNameValidity(AnchorName);
  256. S := CurrentTopicName + '#' + AnchorName;
  257. if ListItemExists(DefinedTopics, S) then
  258. raise Exception.CreateFmt('Anchor name "%s" in topic "%s" defined more than once',
  259. [AnchorName, CurrentTopicName]);
  260. DefinedTopics.Add(S);
  261. Result := Format('<span id="%s">%s</span>', [EscapeHTML(AnchorName), InnerContents]);
  262. end;
  263. function GenerateTopicLinkHTML(const TopicName, AnchorName, InnerContents: String): String;
  264. { Generates HTML for a link to a topic and/or anchor, also updating
  265. TargetTopics }
  266. var
  267. S: String;
  268. begin
  269. if TopicName <> '' then
  270. S := TopicName
  271. else begin
  272. S := CurrentTopicName;
  273. if S = '' then
  274. raise Exception.Create('Cannot create link outside of topic with empty target topic');
  275. if AnchorName = '' then
  276. raise Exception.Create('Cannot create link with neither a target topic nor anchor');
  277. end;
  278. CheckTopicNameValidity(S);
  279. if AnchorName <> '' then begin
  280. CheckAnchorNameValidity(AnchorName);
  281. S := S + '#' + AnchorName;
  282. end;
  283. if not ListItemExists(TargetTopics, S) then
  284. TargetTopics.Add(S);
  285. Result := Format('<a href="%s">%s</a>',
  286. [EscapeHTML(GenerateTopicLink(TopicName, AnchorName)), InnerContents]);
  287. end;
  288. procedure CreateKeyword(const AKeyword, ATopicName, AAnchorName: String);
  289. var
  290. KeywordInfo: TKeywordInfo;
  291. begin
  292. KeywordInfo := TKeywordInfo.Create;
  293. KeywordInfo.Topic := ATopicName;
  294. KeywordInfo.Anchor := AAnchorName;
  295. Keywords.AddObject(AKeyword, KeywordInfo);
  296. end;
  297. function ParseFormattedText(Node: IXMLNode): String;
  298. var
  299. S: String;
  300. I: Integer;
  301. B: Boolean;
  302. begin
  303. Result := '';
  304. Node := Node.FirstChild;
  305. while Assigned(Node) do begin
  306. const Element = ElementFromNode(Node);
  307. case Element of
  308. el_Text:
  309. Result := Result + EscapeHTML(Node.Text, False);
  310. elA:
  311. begin
  312. S := Node.Attributes['name'];
  313. Result := Result + GenerateAnchorHTML(S, ParseFormattedText(Node));
  314. end;
  315. elAnchorLink:
  316. begin
  317. S := Node.Attributes['name'];
  318. Result := Result + GenerateTopicLinkHTML('', S, ParseFormattedText(Node));
  319. end;
  320. elB:
  321. Result := Result + '<b>' + ParseFormattedText(Node) + '</b>';
  322. elBR:
  323. Result := Result + '<br/>';
  324. elDD:
  325. Result := Result + '<dd>' + ParseFormattedText(Node) + '</dd>';
  326. elDL:
  327. Result := Result + '<dl>' + ParseFormattedText(Node) + '</dl>';
  328. elDT:
  329. Result := Result + '<dt>' + ParseFormattedText(Node) + '</dt>';
  330. elExample, elExamples:
  331. begin
  332. Result := Result + '<div class="examplebox">' + SNewLine;
  333. if Node.OptionalAttributes['noheader'] <> '1' then
  334. Result := Result + '<div class="exampleheader">Example' + IfThen(Element = elExamples, 's', '') + ':</div>';
  335. Result := Result + ParseFormattedText(Node) + '</div>';
  336. end;
  337. elFlag:
  338. begin
  339. S := Node.Attributes['name'];
  340. if CurrentTopicName = '' then
  341. raise Exception.Create('<flag> used outside of topic');
  342. CreateKeyword(S, CurrentTopicName, S);
  343. Result := Result + '<dt class="flaglist">' + GenerateAnchorHTML(S, EscapeHTML(S)) +
  344. '</dt>' + SNewLine + '<dd>' + ParseFormattedText(Node) +
  345. '</dd>';
  346. end;
  347. elFlagList:
  348. Result := Result + '<dl>' + ParseFormattedText(Node) + '</dl>';
  349. elI:
  350. Result := Result + '<i>' + ParseFormattedText(Node) + '</i>';
  351. elIndent:
  352. Result := Result + '<div class="indent">' + ParseFormattedText(Node) + '</div>';
  353. elLI:
  354. begin
  355. Result := Result + '<li';
  356. if CurrentListIsCompact then
  357. Result := Result + ' class="compact"';
  358. Result := Result + '>' + ParseFormattedText(Node) + '</li>';
  359. end;
  360. elLink:
  361. begin
  362. S := Node.Attributes['topic'];
  363. Result := Result + GenerateTopicLinkHTML(S, Node.OptionalAttributes['anchor'],
  364. ParseFormattedText(Node));
  365. end;
  366. elExtLink:
  367. begin
  368. S := EscapeHTML(Node.Attributes['href']);
  369. if Pos('ms-its:', S) = 1 then
  370. Result := Result + Format('<a href="%s">%s</a>', [S, ParseFormattedText(Node)])
  371. else
  372. Result := Result + Format('<a href="%s" target="_blank" title="%s">%s</a><img src="images/extlink.png" srcset="images/extlink.svg" alt=" [external link]" />',
  373. [S, S, ParseFormattedText(Node)]);
  374. end;
  375. elHeading:
  376. begin
  377. if IsFirstNonWhitespaceNode(Node) then
  378. Result := Result + '<h2 class="heading notopmargin">'
  379. else
  380. Result := Result + '<h2 class="heading">';
  381. Result := Result + ParseFormattedText(Node) + '</h2>';
  382. end;
  383. elOL:
  384. Result := Result + '<ol>' + ParseFormattedText(Node) + '</ol>';
  385. elP:
  386. begin
  387. if Node.HasAttribute('margin') and (Node.Attributes['margin'] = 'no') then
  388. Result := Result + '<div>' + ParseFormattedText(Node) + '</div>'
  389. else
  390. Result := Result + '<p>' + ParseFormattedText(Node) + '</p>';
  391. end;
  392. elParam:
  393. begin
  394. { IE doesn't support immediate-child-only selectors in CSS (e.g.
  395. "DL.paramlist > DT") so we have to apply the class to each DT
  396. instead of just on the DL. }
  397. S := Node.Attributes['name'];
  398. if CurrentTopicName = '' then
  399. raise Exception.Create('<param> used outside of topic');
  400. CreateKeyword(S, CurrentTopicName, S);
  401. Result := Result + '<dt class="paramlist"><b>' + GenerateAnchorHTML(S, EscapeHTML(S)) + '</b>';
  402. if Node.Attributes['required'] = 'yes' then
  403. Result := Result + ' &nbsp;<i>(Required)</i>';
  404. Result := Result + '</dt><dd class="paramlist">' + ParseFormattedText(Node) + '</dd>';
  405. end;
  406. elParamList:
  407. Result := Result + '<dl>' + ParseFormattedText(Node) + '</dl>';
  408. elPre:
  409. begin
  410. Result := Result + '<pre';
  411. { Special handling for <pre> inside example boxes: Don't include a
  412. bottom margin if <pre> is the last element }
  413. if (ElementFromNode(Node.ParentNode) in [elExample, elExamples]) and
  414. IsLastNonWhitespaceNode(Node) then
  415. Result := Result + ' class="nomargin"';
  416. Result := Result + '>' + ParseFormattedText(Node) + '</pre>';
  417. end;
  418. elPreCode:
  419. Result := Result + '<pre class="indent examplebox">' + ParseFormattedText(Node) + '</pre>';
  420. elSmall:
  421. Result := Result + '<span class="small">' + ParseFormattedText(Node) + '</span>';
  422. elTable:
  423. Result := Result + '<table>' + ParseFormattedText(Node) + '</table>';
  424. elTD:
  425. begin
  426. Result := Result + '<td';
  427. if CurrentTableColumnIndex = 0 then
  428. Result := Result + ' class="cellleft"'
  429. else
  430. Result := Result + ' class="cellright"';
  431. Result := Result + '>' + ParseFormattedText(Node) + '</td>';
  432. Inc(CurrentTableColumnIndex);
  433. end;
  434. elTR:
  435. begin
  436. I := CurrentTableColumnIndex;
  437. CurrentTableColumnIndex := 0;
  438. Result := Result + '<tr>' + ParseFormattedText(Node) + '</tr>';
  439. CurrentTableColumnIndex := I;
  440. end;
  441. elTT:
  442. Result := Result + '<tt>' + ParseFormattedText(Node) + '</tt>';
  443. elU:
  444. Result := Result + '<u>' + ParseFormattedText(Node) + '</u>';
  445. elUL:
  446. begin
  447. B := CurrentListIsCompact;
  448. CurrentListIsCompact := (Node.HasAttribute('appearance') and (Node.Attributes['appearance'] = 'compact'));
  449. Result := Result + '<ul>' + ParseFormattedText(Node) + '</ul>';
  450. CurrentListIsCompact := B;
  451. end;
  452. else
  453. UnexpectedElementError(Node);
  454. end;
  455. Node := Node.NextSibling;
  456. end;
  457. end;
  458. function GenerateSetupDirectiveTopicName(const Directive: String): String;
  459. begin
  460. Result := 'setup_' + Lowercase(Directive);
  461. end;
  462. procedure ParseTopic(const TopicNode: IXMLNode; const SetupTopic: Boolean);
  463. var
  464. TopicDirective, TopicName, TopicTitle: String;
  465. BodyText, SetupFormatText, SetupValidText, SetupDefaultText, S: String;
  466. Node: IXMLNode;
  467. begin
  468. if not SetupTopic then begin
  469. TopicName := TopicNode.Attributes['name'];
  470. TopicTitle := TopicNode.Attributes['title'];
  471. end
  472. else begin
  473. TopicDirective := TopicNode.Attributes['directive'];
  474. TopicName := GenerateSetupDirectiveTopicName(TopicDirective);
  475. CreateKeyword(TopicDirective, TopicName, '');
  476. if TopicNode.HasAttribute('title') then
  477. TopicTitle := '[Setup]: ' + TopicNode.Attributes['title']
  478. else
  479. TopicTitle := '[Setup]: ' + TopicDirective;
  480. end;
  481. CheckTopicNameValidity(TopicName);
  482. if ListItemExists(DefinedTopics, TopicName) then
  483. raise Exception.CreateFmt('Topic "%s" defined more than once', [TopicName]);
  484. DefinedTopics.Add(TopicName);
  485. CurrentTopicName := TopicName;
  486. Node := TopicNode.FirstChild;
  487. while Assigned(Node) do begin
  488. if not IsWhitespace(Node) then begin
  489. case ElementFromNode(Node) of
  490. elBody:
  491. BodyText := ParseFormattedText(Node);
  492. elKeyword:
  493. CreateKeyword(Node.Attributes['value'], TopicName, Node.OptionalAttributes['anchor']);
  494. elSetupDefault:
  495. begin
  496. if not SetupTopic then
  497. raise Exception.Create('<setupdefault> is only valid inside <setuptopic>');
  498. { <div class="margined"> is used instead of <p> since the data could
  499. contain <p>'s of its own, which can't be nested.
  500. NOTE: The space before </div> is intentional -- as noted in
  501. styles.css, "vertical-align: baseline" doesn't work right on IE6,
  502. but putting a space before </div> works around the problem, at
  503. least when it comes to lining up normal text with a single line
  504. of monospaced text. }
  505. SetupDefaultText := '<tr><td class="setuphdrl"><p>Default value:</p></td>' +
  506. '<td class="setuphdrr"><div class="margined">' + ParseFormattedText(Node) +
  507. ' </div></td></tr>' + SNewLine;
  508. end;
  509. elSetupFormat:
  510. begin
  511. if not SetupTopic then
  512. raise Exception.Create('<setupformat> is only valid inside <setuptopic>');
  513. { See comments above! }
  514. SetupFormatText := '<tr><td class="setuphdrl"><p>Format:</p></td>' +
  515. '<td class="setuphdrr"><div class="margined">' + ParseFormattedText(Node) +
  516. ' </div></td></tr>' + SNewLine;
  517. end;
  518. elSetupValid:
  519. begin
  520. if not SetupTopic then
  521. raise Exception.Create('<setupvalid> is only valid inside <setuptopic>');
  522. { See comments above! }
  523. SetupValidText := '<tr><td class="setuphdrl"><p>Valid values:</p></td>' +
  524. '<td class="setuphdrr"><div class="margined">' + ParseFormattedText(Node) +
  525. ' </div></td></tr>' + SNewLine;
  526. end;
  527. else
  528. UnexpectedElementError(Node);
  529. end;
  530. end;
  531. Node := Node.NextSibling;
  532. end;
  533. CurrentTopicName := '';
  534. S :=
  535. '<!DOCTYPE html>' + SNewLine +
  536. '<html lang="en">' + SNewLine +
  537. '<head>' + SNewLine +
  538. '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' + SNewLine +
  539. '<meta http-equiv="X-UA-Compatible" content="IE=11" />' + SNewLine +
  540. '<title>' + EscapeHTML(TopicTitle, False) + '</title>' + SNewLine +
  541. '<link rel="stylesheet" type="text/css" href="styles.css" />' + SNewLine +
  542. '<script type="text/javascript" src="topic.js"></script>' + SNewLine +
  543. '</head>' + SNewLine +
  544. '<body>' + SNewLine +
  545. '<h1 class="topicheading">' + EscapeHTML(TopicTitle, False) + '</h1>' + SNewLine +
  546. '<div class="topicbody">';
  547. if TopicName = 'whatisinnosetup' then begin
  548. S := S + SNewLine + SNewLine +
  549. '<!--[if lt IE 6]>' + SNewLine +
  550. '<p style="background: #ffa0a0; color: black; padding: 6px; border: 1px solid black">' + SNewLine +
  551. 'You are running an old version of Internet Explorer. Consequently, ' +
  552. 'you may encounter problems viewing the documentation. It is ' +
  553. 'recommended that you upgrade to Internet Explorer 6.0 or later.' + SNewLine +
  554. '</p>' + SNewLine +
  555. '<![endif]-->';
  556. end;
  557. if SetupTopic then begin
  558. if (SetupFormatText <> '') or
  559. (SetupValidText <> '') or
  560. (SetupDefaultText <> '') then
  561. S := S + SNewLine + '<table class="setuphdr">' + SNewLine +
  562. SetupFormatText + SetupValidText + SetupDefaultText + '</table>';
  563. S := S + SNewLine + '<div><b>Description:</b></div>';
  564. end;
  565. S := S +
  566. BodyText +
  567. '</div>' + SNewLine +
  568. '</body>' + SNewLine +
  569. '</html>' + SNewLine;
  570. { Normalize the line breaks (MSXML converts CRLF -> LF) }
  571. StringChange(S, #13#10, #10);
  572. StringChange(S, #10, #13#10);
  573. SaveStringToFile(S, OutputDir + GenerateTopicFilename(TopicName));
  574. Inc(TopicsGenerated);
  575. end;
  576. procedure GenerateHTMLHelpContents(const ContentsNode: IXMLNode);
  577. var
  578. SL: TStringList;
  579. procedure AddLeaf(const Title, TopicName: String);
  580. begin
  581. SL.Add(Format('<li><object type="text/sitemap">' +
  582. '<param name="Name" value="%s">' +
  583. '<param name="Local" value="%s"></object>',
  584. [EscapeHTML(Title), EscapeHTML(GenerateTopicLink(TopicName, ''))]));
  585. end;
  586. procedure HandleSetupDirectivesNode;
  587. var
  588. I: Integer;
  589. begin
  590. SL.Add('<ul>');
  591. for I := 0 to SetupDirectives.Count-1 do
  592. AddLeaf(SetupDirectives[I], GenerateSetupDirectiveTopicName(SetupDirectives[I]));
  593. SL.Add('</ul>');
  594. end;
  595. procedure HandleNode(const ParentNode: IXMLNode);
  596. var
  597. Node: IXMLNode;
  598. begin
  599. SL.Add('<ul>');
  600. Node := ParentNode.FirstChild;
  601. while Assigned(Node) do begin
  602. if not IsWhitespace(Node) then begin
  603. case ElementFromNode(Node) of
  604. elContentsHeading:
  605. begin
  606. SL.Add(Format('<li><object type="text/sitemap">' +
  607. '<param name="Name" value="%s"></object>',
  608. [EscapeHTML(Node.Attributes['title'])]));
  609. if Node.Attributes['title'] = '[Setup] section directives' then
  610. HandleSetupDirectivesNode
  611. else
  612. HandleNode(Node);
  613. end;
  614. elContentsTopic:
  615. AddLeaf(Node.Attributes['title'], Node.Attributes['topic']);
  616. else
  617. UnexpectedElementError(Node);
  618. end;
  619. end;
  620. Node := Node.NextSibling;
  621. end;
  622. SL.Add('</ul>');
  623. end;
  624. begin
  625. SL := TStringList.Create;
  626. try
  627. SL.Add('<html><head></head><body>');
  628. HandleNode(ContentsNode);
  629. SL.Add('</body></html>');
  630. SL.WriteBOM := False;
  631. SL.SaveToFile(OutputDir + 'hh_generated_contents.hhc', TEncoding.UTF8);
  632. finally
  633. SL.Free;
  634. end;
  635. end;
  636. procedure GenerateStaticContents(const ContentsNode: IXMLNode);
  637. var
  638. SL: TStringList;
  639. CurHeadingID: Integer;
  640. procedure AddLeaf(const Title, TopicName: String);
  641. begin
  642. SL.Add(Format('<li><a href="%s" target="bodyframe">' +
  643. '<img src="images/contentstopic.svg" alt="" aria-hidden="true" />' +
  644. '<span>%s</span></a></li>',
  645. [EscapeHTML(GenerateTopicLink(TopicName, '')), EscapeHTML(Title)]));
  646. end;
  647. procedure HandleSetupDirectivesNode;
  648. var
  649. I: Integer;
  650. begin
  651. for I := 0 to SetupDirectives.Count-1 do
  652. AddLeaf(SetupDirectives[I], GenerateSetupDirectiveTopicName(SetupDirectives[I]));
  653. end;
  654. procedure HandleNode(const ParentNode: IXMLNode);
  655. var
  656. Node: IXMLNode;
  657. begin
  658. Node := ParentNode.FirstChild;
  659. while Assigned(Node) do begin
  660. if not IsWhitespace(Node) then begin
  661. case ElementFromNode(Node) of
  662. elContentsHeading:
  663. begin
  664. Inc(CurHeadingID);
  665. SL.Add(Format('<li>' +
  666. '<a href="javascript:toggle_node(%d);" aria-controls="nodecontent_%d" aria-expanded="true">' +
  667. '<img src="images/contentsheadopen.svg" alt="'#$25BC' " aria-hidden="true" />' +
  668. '<span>%s</span></a>',
  669. [CurHeadingID, CurHeadingID, EscapeHTML(Node.Attributes['title'])]));
  670. SL.Add(Format('<ul id="nodecontent_%d">', [CurHeadingID]));
  671. if Node.Attributes['title'] = '[Setup] section directives' then
  672. HandleSetupDirectivesNode
  673. else
  674. HandleNode(Node);
  675. SL.Add('</ul></li>');
  676. end;
  677. elContentsTopic:
  678. AddLeaf(Node.Attributes['title'], Node.Attributes['topic']);
  679. else
  680. UnexpectedElementError(Node);
  681. end;
  682. end;
  683. Node := Node.NextSibling;
  684. end;
  685. end;
  686. var
  687. TemplateSL: TStringList;
  688. S: String;
  689. begin
  690. SL := TStringList.Create;
  691. try
  692. CurHeadingID := 0;
  693. SL.Add('<ul>');
  694. HandleNode(ContentsNode);
  695. SL.Add('</ul>');
  696. TemplateSL := TStringList.Create;
  697. try
  698. TemplateSL.LoadFromFile(OutputDir + 'contents-template.htm');
  699. S := TemplateSL.Text;
  700. if StringChange(S, '%CONTENTSTABLES%' + SNewLine, SL.Text) <> 1 then
  701. raise Exception.Create('GenerateStaticContents: Unexpected result from StringChange');
  702. TemplateSL.Text := S;
  703. TemplateSL.WriteBOM := False;
  704. TemplateSL.SaveToFile(OutputDir + 'contents.htm', TEncoding.UTF8);
  705. finally
  706. TemplateSL.Free;
  707. end;
  708. finally
  709. SL.Free;
  710. end;
  711. end;
  712. procedure GenerateHTMLHelpIndex;
  713. function MultiKeyword(const Keyword: String): Boolean;
  714. var
  715. I, N: Integer;
  716. begin
  717. N := 0;
  718. for I := 0 to Keywords.Count-1 do begin
  719. if Keywords[I] = Keyword then begin
  720. Inc(N);
  721. if N > 1 then
  722. Break;
  723. end;
  724. end;
  725. Result := N > 1;
  726. end;
  727. var
  728. SL: TStringList;
  729. I: Integer;
  730. Anchor: String;
  731. begin
  732. SL := TStringList.Create;
  733. try
  734. SL.Add('<html><head></head><body><ul>');
  735. for I := 0 to Keywords.Count-1 do begin
  736. { If a keyword is used more then once, don't use anchors: the 'Topics Found'
  737. dialog displayed when clicking on such a keyword doesn't display the correct
  738. topic titles anymore for each item with an anchor. Some HTML Help bug, see
  739. http://social.msdn.microsoft.com/Forums/en-US/devdocs/thread/a2ee989e-4488-4edd-b034-745ed91c19e2 }
  740. if not MultiKeyword(Keywords[I]) then
  741. Anchor := TKeywordInfo(Keywords.Objects[I]).Anchor
  742. else
  743. Anchor := '';
  744. SL.Add(Format('<li><object type="text/sitemap">' +
  745. '<param name="Name" value="%s">' +
  746. '<param name="Local" value="%s">' +
  747. '</object>',
  748. [EscapeHTML(Keywords[I]),
  749. EscapeHTML(GenerateTopicLink(TKeywordInfo(Keywords.Objects[I]).Topic,
  750. Anchor))]));
  751. end;
  752. SL.Add('</ul></body></html>');
  753. SL.WriteBOM := False;
  754. SL.SaveToFile(OutputDir + 'hh_generated_index.hhk', TEncoding.UTF8);
  755. finally
  756. SL.Free;
  757. end;
  758. end;
  759. procedure GenerateStaticIndex;
  760. function EscapeForJSStringLiteral(const S: String): String;
  761. begin
  762. Result := S;
  763. StringChange(Result, '\', '\\');
  764. StringChange(Result, '"', '\"');
  765. { Note: Escaping " isn't really necessary here since EscapeHTML will
  766. replace all " with &quot; }
  767. end;
  768. var
  769. S, T: String;
  770. I: Integer;
  771. begin
  772. S := 'var contentsIndexData=[';
  773. for I := 0 to Keywords.Count-1 do begin
  774. T := Lowercase(TKeywordInfo(Keywords.Objects[I]).Topic);
  775. if TKeywordInfo(Keywords.Objects[I]).Anchor <> '' then
  776. T := T + '#' + TKeywordInfo(Keywords.Objects[I]).Anchor;
  777. if Pos(':', T) <> 0 then
  778. raise Exception.CreateFmt('GenerateStaticIndex: Invalid character in topic name/anchor "%s"', [T]);
  779. if I <> 0 then
  780. S := S + ',';
  781. S := S + Format('"%s:%s"', [EscapeForJSStringLiteral(EscapeHTML(T)),
  782. EscapeForJSStringLiteral(EscapeHTML(Keywords[I]))]);
  783. end;
  784. S := S + ('];' + SNewLine + 'init_index_tab_elements();');
  785. SaveStringToFile(S, OutputDir + 'contentsindex.js');
  786. end;
  787. procedure CheckForNonexistentTargetTopics;
  788. var
  789. I: Integer;
  790. begin
  791. for I := 0 to TargetTopics.Count-1 do
  792. if not ListItemExists(DefinedTopics, TargetTopics[I]) then
  793. raise Exception.CreateFmt('Link target topic "%s" does not exist',
  794. [TargetTopics[I]]);
  795. //Writeln(Format('Warning: Link target topic "%s" does not exist',
  796. // [TargetTopics[I]]));
  797. end;
  798. procedure Go;
  799. procedure TransformFile(const FromXml, FromXsl, ToXml: String);
  800. var
  801. Doc, StyleDoc: TXMLDocument;
  802. begin
  803. Writeln('- Generating ' + ToXml);
  804. Doc := TXMLDocument.Create;
  805. try
  806. StyleDoc := TXMLDocument.Create;
  807. try
  808. Writeln(' - Loading ' + FromXml);
  809. Doc.LoadFromFile(SourceDir + FromXml);
  810. Writeln(' - Loading ' + FromXsl);
  811. StyleDoc.LoadFromFile(SourceDir + FromXsl);
  812. Writeln(' - Transforming');
  813. SaveStringToFile(Doc.Root.TransformNode(StyleDoc.Root),
  814. SourceDir + ToXml);
  815. finally
  816. StyleDoc.Free;
  817. end;
  818. finally
  819. Doc.Free;
  820. end;
  821. end;
  822. procedure GenerateIsxClassesFile;
  823. var
  824. IsxclassesParser: TIsxclassesParser;
  825. begin
  826. Writeln('- Generating isxclasses_generated.xml');
  827. IsxclassesParser := TIsxclassesParser.Create;
  828. try
  829. IsxclassesParser.Parse(SourceDir + 'isxclasses.pas');
  830. IsxclassesParser.SaveXML(SourceDir + 'isxclasses.header',
  831. SourceDir + 'isxclasses.header2',
  832. SourceDir + 'isxclasses.footer',
  833. SourceDir + 'isxclasses_generated.xml');
  834. IsxclassesParser.SaveWordLists(SourceDir + 'isxclasses_wordlists_generated.pas');
  835. finally
  836. IsxclassesParser.Free;
  837. end;
  838. end;
  839. procedure ReadSetupDirectiveNames(Node: IXMLNode);
  840. begin
  841. while Assigned(Node) do begin
  842. if ElementFromNode(Node) = elSetupTopic then
  843. SetupDirectives.Add(Node.Attributes['directive']);
  844. Node := Node.NextSibling;
  845. end;
  846. end;
  847. procedure DoDoc(Filename: String);
  848. var
  849. Doc: TXMLDocument;
  850. Node: IXMLNode;
  851. begin
  852. Writeln('- Parsing ', Filename);
  853. Doc := TXMLDocument.Create;
  854. try
  855. Doc.LoadFromFile(SourceDir + Filename);
  856. Doc.StripComments;
  857. Node := Doc.Root;
  858. if Node.HasAttribute('version') and (Node.Attributes['version'] <> XMLFileVersion) then
  859. raise Exception.CreateFmt('Unrecognized file version "%s" (expected "%s")',
  860. [Node.Attributes['version'], XMLFileVersion]);
  861. Node := Node.FirstChild;
  862. ReadSetupDirectiveNames(Node);
  863. while Assigned(Node) do begin
  864. if not IsWhitespace(Node) then begin
  865. case ElementFromNode(Node) of
  866. elContents:
  867. begin
  868. Writeln(' - Generating hh_generated_contents.hhc');
  869. GenerateHTMLHelpContents(Node);
  870. if not NoContentsHtm then begin
  871. Writeln(' - Generating contents.htm');
  872. GenerateStaticContents(Node);
  873. end;
  874. end;
  875. elSetupTopic: ParseTopic(Node, True);
  876. elTopic: ParseTopic(Node, False);
  877. else
  878. UnexpectedElementError(Node);
  879. end;
  880. end;
  881. Node := Node.NextSibling;
  882. end;
  883. finally
  884. Doc.Free;
  885. end;
  886. end;
  887. var
  888. I: Integer;
  889. begin
  890. TransformFile('isxfunc.xml', 'isxfunc.xsl', 'isxfunc_generated.xml');
  891. GenerateIsxClassesFile;
  892. TransformFile('ispp.xml', 'ispp.xsl', 'ispp_generated.xml');
  893. Keywords := TStringList.Create;
  894. Keywords.Duplicates := dupAccept;
  895. Keywords.Sorted := True;
  896. DefinedTopics := TStringList.Create;
  897. DefinedTopics.Sorted := True;
  898. TargetTopics := TStringList.Create;
  899. TargetTopics.Sorted := True;
  900. SetupDirectives := TStringList.Create;
  901. SetupDirectives.Duplicates := dupError;
  902. SetupDirectives.Sorted := True;
  903. try
  904. DoDoc('isetup.xml');
  905. DoDoc('isx.xml');
  906. DoDoc('isxfunc_generated.xml');
  907. DoDoc('isxclasses_generated.xml');
  908. DoDoc('ispp_generated.xml');
  909. CheckForNonexistentTargetTopics;
  910. Writeln('- Generating hh_generated_index.hhk');
  911. GenerateHTMLHelpIndex;
  912. if not NoContentsHtm then begin
  913. Writeln('- Generating contentsindex.js');
  914. GenerateStaticIndex;
  915. end;
  916. finally
  917. SetupDirectives.Free;
  918. TargetTopics.Free;
  919. DefinedTopics.Free;
  920. if Assigned(Keywords) then begin
  921. for I := Keywords.Count-1 downto 0 do
  922. TKeywordInfo(Keywords.Objects[I]).Free;
  923. Keywords.Free;
  924. end;
  925. end;
  926. end;
  927. var
  928. StartTime, EndTime: DWORD;
  929. begin
  930. try
  931. Writeln('ISHelpGen v' + Version + ' by Jordan Russell & Martijn Laan');
  932. if (ParamCount = 0) or (ParamCount > 2) then begin
  933. Writeln('usage: ISHelpGen <source-dir> [postfix]');
  934. Halt(2);
  935. end;
  936. SourceDir := ParamStr(1) + '\';
  937. OutputDir := SourceDir + 'Staging' + ParamStr(2) + '\';
  938. NoContentsHtm := not FileExists(OutputDir + 'contents-template.htm');
  939. if NoContentsHtm then
  940. Writeln('Running in NoContentsHtm mode');
  941. OleCheck(CoInitialize(nil)); { for MSXML }
  942. StartTime := GetTickCount;
  943. Go;
  944. EndTime := GetTickCount;
  945. Writeln('Success - ', TopicsGenerated, ' topics generated (',
  946. EndTime - StartTime, ' ms elapsed)');
  947. except
  948. on E: Exception do begin
  949. Writeln('Error: ', TrimRight(E.Message));
  950. Halt(1);
  951. end;
  952. end;
  953. end.