ISHelpGen.dpr 32 KB

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