ISHelpGen.dpr 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031
  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.17';
  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('<a name="%s">%s</a>', [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.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('<tr><td><img src="images/contentstopic.svg" alt="" /></td>' +
  643. '<td><a href="%s" target="bodyframe">%s</a></td></tr>',
  644. [EscapeHTML(GenerateTopicLink(TopicName, '')), EscapeHTML(Title)]));
  645. end;
  646. procedure HandleSetupDirectivesNode;
  647. var
  648. I: Integer;
  649. begin
  650. SL.Add('<table>');
  651. for I := 0 to SetupDirectives.Count-1 do
  652. AddLeaf(SetupDirectives[I], GenerateSetupDirectiveTopicName(SetupDirectives[I]));
  653. SL.Add('</table>');
  654. end;
  655. procedure HandleNode(const ParentNode: IXMLNode);
  656. var
  657. Node: IXMLNode;
  658. begin
  659. SL.Add('<table>');
  660. Node := ParentNode.FirstChild;
  661. while Assigned(Node) do begin
  662. if not IsWhitespace(Node) then begin
  663. case ElementFromNode(Node) of
  664. elContentsHeading:
  665. begin
  666. Inc(CurHeadingID);
  667. SL.Add(Format('<tr id="nodecaption_%d"><td><img id="nodeimg_%d" src="images/contentsheadopen.svg" alt="&gt;&nbsp;" onclick="toggle_node(%d);" /></td>' +
  668. '<td><a href="javascript:toggle_node(%d);">%s</a></td></tr>',
  669. [CurHeadingID, CurHeadingID, CurHeadingID, CurHeadingID, EscapeHTML(Node.Attributes['title'])]));
  670. SL.Add(Format('<tr id="nodecontent_%d"><td></td><td>', [CurHeadingID]));
  671. if Node.Attributes['title'] = '[Setup] section directives' then
  672. HandleSetupDirectivesNode
  673. else
  674. HandleNode(Node);
  675. SL.Add('</td></tr>');
  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. SL.Add('</table>');
  686. end;
  687. var
  688. TemplateSL: TStringList;
  689. S: String;
  690. begin
  691. SL := TStringList.Create;
  692. try
  693. CurHeadingID := 0;
  694. HandleNode(ContentsNode);
  695. TemplateSL := TStringList.Create;
  696. try
  697. TemplateSL.LoadFromFile(OutputDir + 'contents-template.htm');
  698. S := TemplateSL.Text;
  699. if StringChange(S, '%CONTENTSTABLES%' + SNewLine, SL.Text) <> 1 then
  700. raise Exception.Create('GenerateStaticContents: Unexpected result from StringChange');
  701. TemplateSL.Text := S;
  702. TemplateSL.WriteBOM := False;
  703. TemplateSL.SaveToFile(OutputDir + 'contents.htm', TEncoding.UTF8);
  704. finally
  705. TemplateSL.Free;
  706. end;
  707. finally
  708. SL.Free;
  709. end;
  710. end;
  711. procedure GenerateHTMLHelpIndex;
  712. function MultiKeyword(const Keyword: String): Boolean;
  713. var
  714. I, N: Integer;
  715. begin
  716. N := 0;
  717. for I := 0 to Keywords.Count-1 do begin
  718. if Keywords[I] = Keyword then begin
  719. Inc(N);
  720. if N > 1 then
  721. Break;
  722. end;
  723. end;
  724. Result := N > 1;
  725. end;
  726. var
  727. SL: TStringList;
  728. I: Integer;
  729. Anchor: String;
  730. begin
  731. SL := TStringList.Create;
  732. try
  733. SL.Add('<html><head></head><body><ul>');
  734. for I := 0 to Keywords.Count-1 do begin
  735. { If a keyword is used more then once, don't use anchors: the 'Topics Found'
  736. dialog displayed when clicking on such a keyword doesn't display the correct
  737. topic titles anymore for each item with an anchor. Some HTML Help bug, see
  738. http://social.msdn.microsoft.com/Forums/en-US/devdocs/thread/a2ee989e-4488-4edd-b034-745ed91c19e2 }
  739. if not MultiKeyword(Keywords[I]) then
  740. Anchor := TKeywordInfo(Keywords.Objects[I]).Anchor
  741. else
  742. Anchor := '';
  743. SL.Add(Format('<li><object type="text/sitemap">' +
  744. '<param name="Name" value="%s">' +
  745. '<param name="Local" value="%s">' +
  746. '</object>',
  747. [EscapeHTML(Keywords[I]),
  748. EscapeHTML(GenerateTopicLink(TKeywordInfo(Keywords.Objects[I]).Topic,
  749. Anchor))]));
  750. end;
  751. SL.Add('</ul></body></html>');
  752. SL.WriteBOM := False;
  753. SL.SaveToFile(OutputDir + 'hh_generated_index.hhk', TEncoding.UTF8);
  754. finally
  755. SL.Free;
  756. end;
  757. end;
  758. procedure GenerateStaticIndex;
  759. function EscapeForJSStringLiteral(const S: String): String;
  760. begin
  761. Result := S;
  762. StringChange(Result, '\', '\\');
  763. StringChange(Result, '"', '\"');
  764. { Note: Escaping " isn't really necessary here since EscapeHTML will
  765. replace all " with &quot; }
  766. end;
  767. var
  768. S, T: String;
  769. I: Integer;
  770. begin
  771. S := 'var contentsIndexData=[';
  772. for I := 0 to Keywords.Count-1 do begin
  773. T := Lowercase(TKeywordInfo(Keywords.Objects[I]).Topic);
  774. if TKeywordInfo(Keywords.Objects[I]).Anchor <> '' then
  775. T := T + '#' + TKeywordInfo(Keywords.Objects[I]).Anchor;
  776. if Pos(':', T) <> 0 then
  777. raise Exception.CreateFmt('GenerateStaticIndex: Invalid character in topic name/anchor "%s"', [T]);
  778. if I <> 0 then
  779. S := S + ',';
  780. S := S + Format('"%s:%s"', [EscapeForJSStringLiteral(EscapeHTML(T)),
  781. EscapeForJSStringLiteral(EscapeHTML(Keywords[I]))]);
  782. end;
  783. S := S + ('];' + SNewLine + 'init_index_tab_elements();');
  784. SaveStringToFile(S, OutputDir + 'contentsindex.js');
  785. end;
  786. procedure CheckForNonexistentTargetTopics;
  787. var
  788. I: Integer;
  789. begin
  790. for I := 0 to TargetTopics.Count-1 do
  791. if not ListItemExists(DefinedTopics, TargetTopics[I]) then
  792. raise Exception.CreateFmt('Link target topic "%s" does not exist',
  793. [TargetTopics[I]]);
  794. //Writeln(Format('Warning: Link target topic "%s" does not exist',
  795. // [TargetTopics[I]]));
  796. end;
  797. procedure Go;
  798. procedure TransformFile(const FromXml, FromXsl, ToXml: String);
  799. var
  800. Doc, StyleDoc: TXMLDocument;
  801. begin
  802. Writeln('- Generating ' + ToXml);
  803. Doc := TXMLDocument.Create;
  804. try
  805. StyleDoc := TXMLDocument.Create;
  806. try
  807. Writeln(' - Loading ' + FromXml);
  808. Doc.LoadFromFile(SourceDir + FromXml);
  809. Writeln(' - Loading ' + FromXsl);
  810. StyleDoc.LoadFromFile(SourceDir + FromXsl);
  811. Writeln(' - Transforming');
  812. SaveStringToFile(Doc.Root.TransformNode(StyleDoc.Root),
  813. SourceDir + ToXml);
  814. finally
  815. StyleDoc.Free;
  816. end;
  817. finally
  818. Doc.Free;
  819. end;
  820. end;
  821. procedure GenerateIsxClassesFile;
  822. var
  823. IsxclassesParser: TIsxclassesParser;
  824. begin
  825. Writeln('- Generating isxclasses_generated.xml');
  826. IsxclassesParser := TIsxclassesParser.Create;
  827. try
  828. IsxclassesParser.Parse(SourceDir + 'isxclasses.pas');
  829. IsxclassesParser.SaveXML(SourceDir + 'isxclasses.header',
  830. SourceDir + 'isxclasses.header2',
  831. SourceDir + 'isxclasses.footer',
  832. SourceDir + 'isxclasses_generated.xml');
  833. IsxclassesParser.SaveWordLists(SourceDir + 'isxclasses_wordlists_generated.pas');
  834. finally
  835. IsxclassesParser.Free;
  836. end;
  837. end;
  838. procedure ReadSetupDirectiveNames(Node: IXMLNode);
  839. begin
  840. while Assigned(Node) do begin
  841. if ElementFromNode(Node) = elSetupTopic then
  842. SetupDirectives.Add(Node.Attributes['directive']);
  843. Node := Node.NextSibling;
  844. end;
  845. end;
  846. procedure DoDoc(Filename: String);
  847. var
  848. Doc: TXMLDocument;
  849. Node: IXMLNode;
  850. begin
  851. Writeln('- Parsing ', Filename);
  852. Doc := TXMLDocument.Create;
  853. try
  854. Doc.LoadFromFile(SourceDir + Filename);
  855. Doc.StripComments;
  856. Node := Doc.Root;
  857. if Node.HasAttribute('version') and (Node.Attributes['version'] <> XMLFileVersion) then
  858. raise Exception.CreateFmt('Unrecognized file version "%s" (expected "%s")',
  859. [Node.Attributes['version'], XMLFileVersion]);
  860. Node := Node.FirstChild;
  861. ReadSetupDirectiveNames(Node);
  862. while Assigned(Node) do begin
  863. if not IsWhitespace(Node) then begin
  864. case ElementFromNode(Node) of
  865. elContents:
  866. begin
  867. Writeln(' - Generating hh_generated_contents.hhc');
  868. GenerateHTMLHelpContents(Node);
  869. if not NoContentsHtm then begin
  870. Writeln(' - Generating contents.htm');
  871. GenerateStaticContents(Node);
  872. end;
  873. end;
  874. elSetupTopic: ParseTopic(Node, True);
  875. elTopic: ParseTopic(Node, False);
  876. else
  877. UnexpectedElementError(Node);
  878. end;
  879. end;
  880. Node := Node.NextSibling;
  881. end;
  882. finally
  883. Doc.Free;
  884. end;
  885. end;
  886. var
  887. I: Integer;
  888. begin
  889. TransformFile('isxfunc.xml', 'isxfunc.xsl', 'isxfunc_generated.xml');
  890. GenerateIsxClassesFile;
  891. TransformFile('ispp.xml', 'ispp.xsl', 'ispp_generated.xml');
  892. Keywords := TStringList.Create;
  893. Keywords.Duplicates := dupAccept;
  894. Keywords.Sorted := True;
  895. DefinedTopics := TStringList.Create;
  896. DefinedTopics.Sorted := True;
  897. TargetTopics := TStringList.Create;
  898. TargetTopics.Sorted := True;
  899. SetupDirectives := TStringList.Create;
  900. SetupDirectives.Duplicates := dupError;
  901. SetupDirectives.Sorted := True;
  902. try
  903. DoDoc('isetup.xml');
  904. DoDoc('isx.xml');
  905. DoDoc('isxfunc_generated.xml');
  906. DoDoc('isxclasses_generated.xml');
  907. DoDoc('ispp_generated.xml');
  908. CheckForNonexistentTargetTopics;
  909. Writeln('- Generating hh_generated_index.hhk');
  910. GenerateHTMLHelpIndex;
  911. if not NoContentsHtm then begin
  912. Writeln('- Generating contentsindex.js');
  913. GenerateStaticIndex;
  914. end;
  915. finally
  916. SetupDirectives.Free;
  917. TargetTopics.Free;
  918. DefinedTopics.Free;
  919. if Assigned(Keywords) then begin
  920. for I := Keywords.Count-1 downto 0 do
  921. TKeywordInfo(Keywords.Objects[I]).Free;
  922. Keywords.Free;
  923. end;
  924. end;
  925. end;
  926. var
  927. StartTime, EndTime: DWORD;
  928. begin
  929. try
  930. Writeln('ISHelpGen v' + Version + ' by Jordan Russell & Martijn Laan');
  931. if (ParamCount = 0) or (ParamCount > 2) then begin
  932. Writeln('usage: ISHelpGen <source-dir> [postfix]');
  933. Halt(2);
  934. end;
  935. SourceDir := ParamStr(1) + '\';
  936. OutputDir := SourceDir + 'Staging' + ParamStr(2) + '\';
  937. NoContentsHtm := not FileExists(OutputDir + 'contents-template.htm');
  938. if NoContentsHtm then
  939. Writeln('Running in NoContentsHtm mode');
  940. OleCheck(CoInitialize(nil)); { for MSXML }
  941. StartTime := GetTickCount;
  942. Go;
  943. EndTime := GetTickCount;
  944. Writeln('Success - ', TopicsGenerated, ' topics generated (',
  945. EndTime - StartTime, ' ms elapsed)');
  946. except
  947. on E: Exception do begin
  948. Writeln('Error: ', TrimRight(E.Message));
  949. Halt(1);
  950. end;
  951. end;
  952. end.