topicLinker.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  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. const fullRef = ref + (anchorId ? '#' + anchorId : '');
  42. const 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({
  48. class: cssClassPrefix + 'doc-internal-link',
  49. href: 'javascript:void(0);',
  50. 'data-doc-ref': ref
  51. });
  52. if (anchorId) {
  53. node.attr('data-doc-anchor-id', anchorId);
  54. }
  55. } else {
  56. // Here the topic is unknown or not part of the main topic tree so we make an external link instead
  57. const href = EXTERNAL_DOC_URL_PREFIX + fullRef.replace('.xml', '.html');
  58. replaceWithExternalLink(node, href, cssClassPrefix);
  59. }
  60. if (!node.text().trim()) {
  61. if (fragmentSearchResult.fragment) {
  62. fragmentSearchResult.fragment.title.childNodes().forEach(titleChild => {
  63. node.addChild(titleChild.clone());
  64. });
  65. } else {
  66. console.log('%s: Could not find suitable text for: %s#%s', LOG_NAME, ref, anchorId);
  67. }
  68. }
  69. };
  70. /**
  71. * Replaces keyword nodes with they're specified value.
  72. *
  73. * @param node
  74. * @param {DitamapParseResult[]} parseResults
  75. * @param {String} cssClassPrefix
  76. */
  77. const handleKeywordNode = (node, parseResults, cssClassPrefix) => {
  78. if (!extractorUtils.hasAttributes(node, 'keyref')) {
  79. throw new Error('Found keyword without keyref attribute.');
  80. return;
  81. }
  82. const keyRef = node.attr('keyref').value();
  83. node.attr('keyref').remove();
  84. let keyDef = undefined;
  85. parseResults.some(parseResult => {
  86. if (parseResult.keyDefs[keyRef]) {
  87. keyDef = parseResult.keyDefs[keyRef];
  88. return true;
  89. }
  90. });
  91. if (!keyDef) {
  92. // Here there's no corresponding key definition parsed
  93. if (node.text().trim()) {
  94. node.replace(node.text());
  95. } else if (keyRef === 'distro') {
  96. // The 'distro' keyword is likely in the top level of the docs, and not part of any sub-maps so default to 'CDH'.
  97. node.replace('CDH');
  98. } else {
  99. // Haven't seen this case yet
  100. throw new Error('Unknown keyref found: ' + keyRef);
  101. }
  102. return;
  103. }
  104. if (keyDef.text && !keyDef.href) {
  105. // A simple reference to a text
  106. node.replace(keyDef.text);
  107. return;
  108. }
  109. if (keyDef.href) {
  110. if (keyDef.external) {
  111. replaceWithExternalLink(node, keyDef.href, cssClassPrefix);
  112. if (!node.text().trim()) {
  113. node.text(keyDef.text || keyDef.href);
  114. }
  115. } else {
  116. if (keyDef.href.indexOf('scalability_file_handle_cache') !== -1) {
  117. }
  118. replaceWithInternalLink(
  119. node,
  120. keyDef.href.replace(/#.*$/, ''),
  121. keyRef,
  122. parseResults,
  123. cssClassPrefix
  124. );
  125. }
  126. return;
  127. }
  128. throw new Error('Failed handling keyword node.');
  129. };
  130. /**
  131. * Replaces xref nodes with links
  132. *
  133. * @param node
  134. * @param {DitamapParseResult[]} parseResults
  135. * @param {string} cssClassPrefix
  136. */
  137. const handkeXrefNode = (node, parseResults, cssClassPrefix) => {
  138. if (extractorUtils.hasAttributes(node, 'href')) {
  139. const href = node.attr('href').value();
  140. if (node.attr('scope') && node.attr('scope').value() === 'external') {
  141. replaceWithExternalLink(node, href, cssClassPrefix);
  142. if (!node.text()) {
  143. node.text(href);
  144. }
  145. return;
  146. }
  147. const ref = ~href.indexOf('#') ? href.replace(/#.*$/, '') : href;
  148. const anchorId = ~href.indexOf('#') && href.replace(/^.*#/, '');
  149. replaceWithInternalLink(node, ref, anchorId, parseResults, cssClassPrefix);
  150. }
  151. if (extractorUtils.hasAttributes(node, 'keyref')) {
  152. handleKeywordNode(node, parseResults, cssClassPrefix);
  153. }
  154. };
  155. /**
  156. * Generates a table of contents if the topic has children
  157. *
  158. * @param {Topic} topic
  159. * @param node
  160. */
  161. const handleTocNode = (topic, node) => {
  162. if (topic.children.length) {
  163. node.name('div');
  164. const header = node.node('div');
  165. header.text('Continue reading:');
  166. const ul = node.node('ul');
  167. topic.children.forEach(childTopic => {
  168. const li = ul.node('li');
  169. const xrefNode = li.node('xref');
  170. xrefNode.attr('href', childTopic.ref);
  171. });
  172. } else {
  173. node.remove();
  174. }
  175. };
  176. /**
  177. * Recursively identifies and replaces certain elements with the previous parse results.
  178. *
  179. * @param node
  180. * @param {DitamapParseResult[]} parseResults
  181. * @param {string} cssClassPrefix
  182. * @param {Object} foundCssClasses - Map keeping track of all the added css classes
  183. * @return {Object} - The map of added css classes
  184. */
  185. const linkNodesInDomXml = (node, parseResults, cssClassPrefix, foundCssClasses) => {
  186. switch (node.name()) {
  187. case 'keyword':
  188. handleKeywordNode(node, parseResults, cssClassPrefix);
  189. break;
  190. case 'xref':
  191. handkeXrefNode(node, parseResults, cssClassPrefix);
  192. break;
  193. case 'image':
  194. throw new Error('Unsupported image element found');
  195. case 'imagemap':
  196. throw new Error('Unsupported imagemap element found');
  197. default:
  198. break;
  199. }
  200. if (extractorUtils.hasAttributes(node, 'class')) {
  201. foundCssClasses[node.attr('class').value()] = true;
  202. }
  203. node
  204. .childNodes()
  205. .forEach(childNode =>
  206. linkNodesInDomXml(childNode, parseResults, cssClassPrefix, foundCssClasses)
  207. );
  208. return foundCssClasses;
  209. };
  210. /**
  211. * Adds conrefs and table of contents before the linking of the topics.
  212. *
  213. * @param {Topic} topic
  214. * @param node
  215. * @param {DitamapParseResult[]} parseResults
  216. */
  217. const insertConrefsAndToc = (topic, node, parseResults) => {
  218. if (node.name() === 'toc') {
  219. handleTocNode(topic, node);
  220. }
  221. if (extractorUtils.hasAttributes(node, 'conref')) {
  222. const conref = node.attr('conref').value();
  223. const splitRef = conref.split('#');
  224. const fragmentSearchResult = extractorUtils.findFragment(
  225. parseResults,
  226. splitRef[0],
  227. splitRef[1]
  228. );
  229. if (!fragmentSearchResult.fragment) {
  230. console.log(node.toString());
  231. console.log('%s: Could not find fragment for conref: %s', LOG_NAME, conref);
  232. } else {
  233. node = node.replace(fragmentSearchResult.fragment.domElement.clone());
  234. if (extractorUtils.hasAttributes(node, 'id')) {
  235. node.attr('id').remove();
  236. }
  237. }
  238. }
  239. node.childNodes().forEach(childNode => insertConrefsAndToc(topic, childNode, parseResults));
  240. };
  241. /**
  242. * Links all the nodes in a topic
  243. *
  244. * @param {Topic} topic
  245. * @param {DitamapParseResult[]} parseResults
  246. * @param {String} cssClassPrefix
  247. * @param {Object} foundCssClasses
  248. */
  249. const linkNodesInTopic = (topic, parseResults, cssClassPrefix, foundCssClasses) => {
  250. // First insert all the conrefs and topics
  251. insertConrefsAndToc(topic, topic.domXml, parseResults);
  252. // Then deal with xrefs, keywords etc.
  253. linkNodesInDomXml(topic.domXml, parseResults, cssClassPrefix, foundCssClasses);
  254. topic.children.forEach(childTopic =>
  255. linkNodesInTopic(childTopic, parseResults, cssClassPrefix, foundCssClasses)
  256. );
  257. };
  258. /**
  259. * This links all the nodes after parsing, it replaces keywords, adds links for xrefs, builds table of contents etc.
  260. *
  261. * @param parseResults
  262. * @param cssClassPrefix
  263. */
  264. const linkTopics = (parseResults, cssClassPrefix) => {
  265. const foundCssClasses = {};
  266. parseResults.forEach(parseResult =>
  267. parseResult.topics.forEach(topic => {
  268. linkNodesInTopic(topic, parseResults, cssClassPrefix, foundCssClasses);
  269. })
  270. );
  271. console.log('%s: Found CSS classes: %s', LOG_NAME, Object.keys(foundCssClasses).join(','));
  272. };
  273. module.exports = {
  274. linkTopics: linkTopics
  275. };