topicLinker.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. // Licensed to Cloudera, Inc. under one
  2. // or more contributor license agreements. See the NOTICE file
  3. // distributed with this work for additional information
  4. // regarding copyright ownership. Cloudera, Inc. licenses this file
  5. // to you under the Apache License, Version 2.0 (the
  6. // "License"); you may not use this file except in compliance
  7. // with the License. You may obtain a copy of the License at
  8. //
  9. // http://www.apache.org/licenses/LICENSE-2.0
  10. //
  11. // Unless required by applicable law or agreed to in writing, software
  12. // distributed under the License is distributed on an "AS IS" BASIS,
  13. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. // See the License for the specific language governing permissions and
  15. // limitations under the License.
  16. const extractorUtils = require('./extractorUtils');
  17. const EXTERNAL_DOC_URL_PREFIX = 'https://www.cloudera.com/documentation/enterprise/latest/';
  18. const LOG_NAME = 'topicLinker.js';
  19. /**
  20. * Creates an external link
  21. *
  22. * @param node
  23. * @param {string} href
  24. * @param {string} [cssClassPrefix]
  25. */
  26. const replaceWithExternalLink = (node, href, cssClassPrefix) => {
  27. node.name('a');
  28. extractorUtils.removeAllAttributes(node);
  29. node.attr({ 'class': cssClassPrefix + 'doc-external-link', 'href': href, 'target': '_blank' });
  30. };
  31. /**
  32. * Creates an internal link if the the ref is part of the topic tree, otherwise it will create an external link.
  33. *
  34. * @param node
  35. * @param {string} ref
  36. * @param {string} anchorId
  37. * @param {DitamapParseResult[]} parseResults
  38. * @param {string} cssClassPrefix
  39. */
  40. const replaceWithInternalLink = (node, ref, anchorId, parseResults, cssClassPrefix) => {
  41. let fullRef = ref + (anchorId ? '#' + anchorId : '');
  42. let fragmentSearchResult = extractorUtils.findFragment(parseResults, ref, anchorId);
  43. if (fragmentSearchResult.fragment && fragmentSearchResult.partOfTree) {
  44. // Here the topic is parsed and it's part of the main topic tree
  45. node.name('a');
  46. extractorUtils.removeAllAttributes(node);
  47. node.attr({ 'class': cssClassPrefix + 'doc-internal-link', 'href': 'javascript:void(0);', 'data-doc-ref': ref });
  48. if (anchorId) {
  49. node.attr('data-doc-anchor-id', anchorId);
  50. }
  51. } else {
  52. // Here the topic is unknown or not part of the main topic tree so we make an external link instead
  53. let href = EXTERNAL_DOC_URL_PREFIX + fullRef.replace('.xml', '.html');
  54. replaceWithExternalLink(node, href, cssClassPrefix);
  55. }
  56. if (!node.text().trim()) {
  57. if (fragmentSearchResult.fragment) {
  58. fragmentSearchResult.fragment.title.childNodes().forEach(titleChild => {
  59. node.addChild(titleChild.clone());
  60. });
  61. } else {
  62. console.log('%s: Could not find suitable text for: %s#%s', LOG_NAME, ref, anchorId);
  63. }
  64. }
  65. };
  66. /**
  67. * Replaces keyword nodes with they're specified value.
  68. *
  69. * @param node
  70. * @param {DitamapParseResult[]} parseResults
  71. * @param {String} cssClassPrefix
  72. */
  73. const handleKeywordNode = (node, parseResults, cssClassPrefix) => {
  74. if (!extractorUtils.hasAttributes(node, 'keyref')) {
  75. throw new Error('Found keyword without keyref attribute.');
  76. return;
  77. }
  78. let keyRef = node.attr('keyref').value();
  79. node.attr('keyref').remove();
  80. let keyDef = undefined;
  81. parseResults.some(parseResult => {
  82. if (parseResult.keyDefs[keyRef]) {
  83. keyDef = parseResult.keyDefs[keyRef];
  84. return true;
  85. }
  86. });
  87. if (!keyDef) {
  88. // Here there's no corresponding key definition parsed
  89. if (node.text().trim()) {
  90. node.replace(node.text());
  91. } else if (keyRef === 'distro') {
  92. // The 'distro' keyword is likely in the top level of the docs, and not part of any sub-maps so default to 'CDH'.
  93. node.replace('CDH');
  94. } else {
  95. // Haven't seen this case yet
  96. throw new Error('Unknown keyref found: ' + keyRef);
  97. }
  98. return;
  99. }
  100. if (keyDef.text && !keyDef.href) {
  101. // A simple reference to a text
  102. node.replace(keyDef.text);
  103. return;
  104. }
  105. if (keyDef.href) {
  106. if (keyDef.external) {
  107. replaceWithExternalLink(node, keyDef.href, cssClassPrefix);
  108. if (!node.text().trim()) {
  109. node.text(keyDef.text || keyDef.href);
  110. }
  111. } else {
  112. if (keyDef.href.indexOf('scalability_file_handle_cache') !== -1) {
  113. }
  114. replaceWithInternalLink(node, keyDef.href.replace(/#.*$/, ''), keyRef, parseResults, cssClassPrefix);
  115. }
  116. return;
  117. }
  118. throw new Error('Failed handling keyword node.')
  119. };
  120. /**
  121. * Replaces xref nodes with links
  122. *
  123. * @param node
  124. * @param {DitamapParseResult[]} parseResults
  125. * @param {string} cssClassPrefix
  126. */
  127. let handkeXrefNode = (node, parseResults, cssClassPrefix) => {
  128. if (extractorUtils.hasAttributes(node, 'href')) {
  129. let href = node.attr('href').value();
  130. if (node.attr('scope') && node.attr('scope').value() === 'external') {
  131. replaceWithExternalLink(node, href, cssClassPrefix);
  132. if (!node.text()) {
  133. node.text(href)
  134. }
  135. return;
  136. }
  137. let ref = ~href.indexOf('#') ? href.replace(/#.*$/, '') : href;
  138. let anchorId = ~href.indexOf('#') && href.replace(/^.*#/, '');
  139. replaceWithInternalLink(node, ref, anchorId, parseResults, cssClassPrefix);
  140. }
  141. if (extractorUtils.hasAttributes(node, 'keyref')) {
  142. handleKeywordNode(node, parseResults, cssClassPrefix);
  143. }
  144. };
  145. /**
  146. * Generates a table of contents if the topic has children
  147. *
  148. * @param {Topic} topic
  149. * @param node
  150. */
  151. const handleTocNode = (topic, node) => {
  152. if (topic.children.length) {
  153. node.name('div');
  154. let header = node.node('div');
  155. header.text('Continue reading:');
  156. let ul = node.node('ul');
  157. topic.children.forEach(childTopic => {
  158. let li = ul.node('li');
  159. let xrefNode = li.node('xref');
  160. xrefNode.attr('href', childTopic.ref);
  161. })
  162. } else {
  163. node.remove();
  164. }
  165. };
  166. /**
  167. * Recursively identifies and replaces certain elements with the previous parse results.
  168. *
  169. * @param node
  170. * @param {DitamapParseResult[]} parseResults
  171. * @param {string} cssClassPrefix
  172. * @param {Object} foundCssClasses - Map keeping track of all the added css classes
  173. * @return {Object} - The map of added css classes
  174. */
  175. const linkNodesInDomXml = (node, parseResults, cssClassPrefix, foundCssClasses) => {
  176. switch (node.name()) {
  177. case 'keyword':
  178. handleKeywordNode(node, parseResults, cssClassPrefix);
  179. break;
  180. case 'xref':
  181. handkeXrefNode(node, parseResults, cssClassPrefix);
  182. break;
  183. case 'image':
  184. throw new Error('Unsupported image element found');
  185. case 'imagemap':
  186. throw new Error('Unsupported imagemap element found');
  187. default:
  188. break;
  189. }
  190. if (extractorUtils.hasAttributes(node, 'class')) {
  191. foundCssClasses[node.attr('class').value()] = true;
  192. }
  193. node.childNodes().forEach(childNode => linkNodesInDomXml(childNode, parseResults, cssClassPrefix, foundCssClasses));
  194. return foundCssClasses;
  195. };
  196. /**
  197. * Adds conrefs and table of contents before the linking of the topics.
  198. *
  199. * @param {Topic} topic
  200. * @param node
  201. * @param {DitamapParseResult[]} parseResults
  202. */
  203. const insertConrefsAndToc = (topic, node, parseResults) => {
  204. if (node.name() === 'toc') {
  205. handleTocNode(topic, node);
  206. }
  207. if (extractorUtils.hasAttributes(node, 'conref')) {
  208. let conref = node.attr('conref').value();
  209. var splitRef = conref.split('#');
  210. let fragmentSearchResult = extractorUtils.findFragment(parseResults, splitRef[0], splitRef[1]);
  211. if (!fragmentSearchResult.fragment) {
  212. console.log(node.toString());
  213. console.log('%s: Could not find fragment for conref: %s', LOG_NAME, conref);
  214. } else {
  215. node = node.replace(fragmentSearchResult.fragment.domElement.clone());
  216. if (extractorUtils.hasAttributes(node, 'id')) {
  217. node.attr('id').remove();
  218. }
  219. }
  220. }
  221. node.childNodes().forEach(childNode => insertConrefsAndToc(topic, childNode, parseResults));
  222. };
  223. /**
  224. * Links all the nodes in a topic
  225. *
  226. * @param {Topic} topic
  227. * @param {DitamapParseResult[]} parseResults
  228. * @param {String} cssClassPrefix
  229. * @param {Object} foundCssClasses
  230. */
  231. let linkNodesInTopic = (topic, parseResults, cssClassPrefix, foundCssClasses) => {
  232. // First insert all the conrefs and topics
  233. insertConrefsAndToc(topic, topic.domXml, parseResults);
  234. // Then deal with xrefs, keywords etc.
  235. linkNodesInDomXml(topic.domXml, parseResults, cssClassPrefix, foundCssClasses);
  236. topic.children.forEach(childTopic => linkNodesInTopic(childTopic, parseResults, cssClassPrefix, foundCssClasses));
  237. };
  238. /**
  239. * This links all the nodes after parsing, it replaces keywords, adds links for xrefs, builds table of contents etc.
  240. *
  241. * @param parseResults
  242. * @param cssClassPrefix
  243. */
  244. const linkTopics = (parseResults, cssClassPrefix) => {
  245. let foundCssClasses = {};
  246. parseResults.forEach(parseResult => parseResult.topics.forEach(topic => {
  247. linkNodesInTopic(topic, parseResults, cssClassPrefix, foundCssClasses);
  248. }));
  249. console.log('%s: Found CSS classes: %s', LOG_NAME, Object.keys(foundCssClasses).join(','));
  250. };
  251. module.exports = {
  252. linkTopics: linkTopics
  253. };