topicLinker.js 9.1 KB

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