docXmlParser.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  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 DocFragment = require('./DocFragment');
  17. const Topic = require('./Topic');
  18. const extractorUtils = require('./extractorUtils');
  19. const libxml = require('libxmljs');
  20. const LOG_NAME = 'docXmlParser.js';
  21. const isHidden = (docElement) => docElement.attr('audience') &&
  22. (docElement.attr('audience').value() === 'hidden' || docElement.attr('audience').value() === 'PDF');
  23. // Turn relative anchor or topic links into absolute
  24. const makeAbsoluteRef = (href, topic) => {
  25. if (href.indexOf('#') === 0) {
  26. return topic.ref + href;
  27. }
  28. if (href.indexOf('#') === -1 && href.indexOf('.xml') === -1) {
  29. return topic.ref + '#' + href;
  30. }
  31. if (/^[^/]+\.xml.*$/.test(href) && ~topic.ref.indexOf('/')) {
  32. // Path is relative current doc (add parent folders if exists)
  33. return extractorUtils.getParentFolder(topic.ref) + href;
  34. }
  35. if (href.indexOf('..') !== -1) {
  36. // Make relative parent paths relative to start folder
  37. return (extractorUtils.getParentFolder(topic.ref) + href).replace(/[^/]+\/\.\.\//g, '');
  38. }
  39. return href;
  40. };
  41. const parseTopic = (topic, cssClassPrefix, conrefCallback) => {
  42. return new Promise((resolve, reject) => {
  43. extractorUtils.readFile(topic.docRootPath + (~topic.ref.indexOf('#') ? topic.ref.replace(/#.*$/, '') : topic.ref)).then(contents => {
  44. let xmlDoc = libxml.parseXmlString(contents);
  45. let docElement = xmlDoc.root();
  46. if (~topic.ref.indexOf('#')) {
  47. docElement = docElement.get('//*[@id=\'' + topic.ref.replace(/^.*#/, '') + '\']')
  48. }
  49. parseDocElement(docElement, topic.domXml, cssClassPrefix, topic, undefined, conrefCallback);
  50. resolve();
  51. }).catch(reject);
  52. })
  53. };
  54. const parseDocElement = (docElement, domElement, cssClassPrefix, topic, activeFragment, conrefCallback) => {
  55. // return in the switch stops the recursion at this node
  56. if (extractorUtils.hasAttributes(docElement, 'conref')) {
  57. let absoluteConRef = makeAbsoluteRef(docElement.attr('conref').value(), topic);
  58. docElement.attr('conref', absoluteConRef);
  59. conrefCallback(topic, absoluteConRef.replace(/#.*$/, ''));
  60. }
  61. if (docElement.attr('outputclass') && docElement.attr('outputclass').value() === 'toc') {
  62. domElement.node('toc');
  63. return;
  64. }
  65. switch (docElement.name()) {
  66. case 'concept':
  67. case 'conbody':
  68. domElement = domElement.node('div');
  69. break;
  70. case 'tgroup':
  71. case 'colspec':
  72. case 'dlentry': // TODO: Possibly keep track of ID if re-used elsewhere, for now ignore.
  73. // skip
  74. break;
  75. case 'alt':
  76. case 'area':
  77. case 'b':
  78. case 'cite':
  79. case 'coords':
  80. case 'dd':
  81. case 'dl':
  82. case 'dt':
  83. case 'fn':
  84. case 'i':
  85. case 'li':
  86. case 'ol':
  87. case 'p':
  88. case 'shape':
  89. case 'q':
  90. case 'sup':
  91. case 'table':
  92. case 'tbody':
  93. case 'thead':
  94. case 'tt':
  95. case 'u':
  96. case 'ul':
  97. if (isHidden(docElement)) {
  98. return;
  99. }
  100. domElement = domElement.node(docElement.name());
  101. if (extractorUtils.hasAttributes(docElement, 'conref')) {
  102. domElement.attr('conref', docElement.attr('conref').value());
  103. }
  104. break;
  105. case 'sthead':
  106. domElement = domElement.node('tr');
  107. domElement.attr({ 'class': cssClassPrefix + 'doc-sthead' });
  108. break;
  109. case 'stentry':
  110. domElement = domElement.node('td');
  111. break;
  112. case 'simpletable':
  113. domElement = domElement.node('table');
  114. break;
  115. case 'strow':
  116. case 'row':
  117. domElement = domElement.node('tr');
  118. break;
  119. case 'entry':
  120. if (docElement.parent().name().toLowerCase() === 'row') {
  121. domElement = domElement.node('td');
  122. } else {
  123. console.log('%s: Got "entry" element without a parent "row": %s in ref %s', LOG_NAME, docElement.toString(), topic.ref);
  124. return;
  125. }
  126. break;
  127. case 'xref':
  128. if (extractorUtils.hasAttributes(docElement, 'href') && (!docElement.attr('scope') || docElement.attr('scope').value() !== 'external')) {
  129. docElement.attr('href', makeAbsoluteRef(docElement.attr('href').value(), topic));
  130. }
  131. case 'image':
  132. case 'imagemap':
  133. case 'keyword':
  134. // These elements are dealt with later, we don't deep clone as there might be child elements to parse
  135. domElement = domElement.node(docElement.name());
  136. docElement.attrs().forEach(attr => {
  137. domElement.attr(attr.name(), attr.value())
  138. });
  139. break;
  140. case 'object':
  141. if (extractorUtils.hasAttributes(docElement, ['data', 'outputclass'])) {
  142. domElement = domElement.node('iframe');
  143. domElement.attr({ 'class': cssClassPrefix + 'doc-iframe', 'src': docElement.attr('data').value() });
  144. if (extractorUtils.hasAttributes(docElement, 'width')) {
  145. domElement.attr({ 'width': docElement.attr('width').value() });
  146. }
  147. if (extractorUtils.hasAttributes(docElement, 'height')) {
  148. domElement.attr({ 'height': docElement.attr('height').value() });
  149. }
  150. } else {
  151. console.log('%s: Got "object" element without data and outputclass: %s in ref %s', LOG_NAME, docElement.toString(), topic.ref);
  152. return;
  153. }
  154. break;
  155. case 'pre': // Enables better styling if div + class
  156. case 'cmdname':
  157. case 'codeph':
  158. case 'filepath':
  159. case 'lines':
  160. case 'option':
  161. case 'parmname':
  162. case 'ph':
  163. case 'systemoutput':
  164. case 'term':
  165. case 'userinput':
  166. case 'apiname':
  167. case 'varname':
  168. if (isHidden(docElement)) {
  169. return;
  170. }
  171. domElement = domElement.node('span');
  172. domElement.attr({ 'class': cssClassPrefix + 'doc-' + docElement.name() });
  173. break;
  174. case 'codeblock':
  175. case 'conbodydiv':
  176. case 'example':
  177. case 'fig':
  178. case 'menucascade':
  179. case 'msgblock':
  180. case 'note':
  181. case 'section':
  182. case 'sectiondiv':
  183. case 'title':
  184. case 'uicontrol':
  185. if (isHidden(docElement)) {
  186. return;
  187. }
  188. domElement = domElement.node('div');
  189. domElement.attr({ 'class': cssClassPrefix + 'doc-' + docElement.name() });
  190. if (docElement.name() === 'title' && activeFragment && !activeFragment.title) {
  191. activeFragment.title = domElement;
  192. }
  193. break;
  194. case 'text':
  195. if (docElement.text().trim()) {
  196. let firstInDiv = domElement.name() === 'div' && domElement.childNodes().length === 0;
  197. domElement = domElement.node('text');
  198. domElement.replace(firstInDiv ? docElement.text().replace(/^[\n\r]*/, '') : docElement.text());
  199. }
  200. break;
  201. case 'abstract':
  202. case 'comment':
  203. case 'data':
  204. case 'draft-comment':
  205. case 'indexterm':
  206. case 'oxy_attributes':
  207. case 'oxy_comment_start':
  208. case 'oxy_comment_end':
  209. case 'oxy_delete':
  210. case 'oxy_insert_start':
  211. case 'oxy_insert_end':
  212. case 'prolog':
  213. case 'shortdesc':
  214. case 'titlealts':
  215. return;
  216. case undefined:
  217. if (/^<\!\[cdata.*/i.test(docElement.toString())) {
  218. if (docElement.text().trim()) {
  219. let firstInDiv = domElement.name() === 'div' && domElement.childNodes().length === 0;
  220. domElement = domElement.node('text');
  221. domElement.replace(firstInDiv ? docElement.text().replace(/^[\n\r]*/, '') : docElement.text());
  222. }
  223. break;
  224. }
  225. default:
  226. console.log('%s: Can\'t handle node: %s in ref %s', LOG_NAME, docElement.name(), topic.ref);
  227. return;
  228. }
  229. if (isHidden(docElement)) {
  230. domElement.attr({ 'style': 'display:none;' });
  231. }
  232. if (extractorUtils.hasAttributes(docElement, 'id')) {
  233. let fragmentId = docElement.attr('id') && docElement.attr('id').value();
  234. let newFragment = new DocFragment(fragmentId, domElement);
  235. if (!topic.fragment) {
  236. topic.fragment = newFragment;
  237. } else {
  238. activeFragment.children.push(newFragment);
  239. }
  240. activeFragment = newFragment;
  241. }
  242. if (extractorUtils.hasAttributes(docElement, 'conref') && !extractorUtils.hasAttributes(domElement, 'conref')) {
  243. domElement.attr('conref', docElement.attr('conref').value());
  244. }
  245. docElement.childNodes().forEach(childNode => parseDocElement(childNode, domElement, cssClassPrefix, topic, activeFragment, conrefCallback));
  246. };
  247. /**
  248. * Parses all the topic xml files and sets the intermediary DOM on the topic, after this linkage is required to insert
  249. * any conrefs or keywords etc. that are only known after parsing all the topics.
  250. *
  251. * @param parseResults
  252. * @param cssClassPrefix
  253. * @return {Promise}
  254. */
  255. const parseTopics = (parseResults, cssClassPrefix) => new Promise((resolve, reject) => {
  256. let topicIndex = {};
  257. let topicsToParse = [];
  258. let populateTopicsFromTree = topics => {
  259. topics.forEach(topic => {
  260. topicsToParse.push(topic);
  261. topicIndex[topic.ref] = true;
  262. populateTopicsFromTree(topic.children);
  263. })
  264. };
  265. // Topics might be referenced from within .xml files thar are not part of the ditamap, we add them here to make
  266. // sure they're parsed
  267. let conrefCallback = (sourceTopic, ref) => {
  268. if (!topicIndex[ref]) {
  269. let topic = new Topic(sourceTopic.docRootPath, ref);
  270. topicIndex[ref] = true;
  271. topicsToParse.push(topic);
  272. if (parseResults.length < 2) {
  273. // We add additional topics to any ditamap parseresults except the first one, this prevents
  274. // them from being part of the tree.
  275. parseResults.push({
  276. topics: [],
  277. topicIndex: {},
  278. keyDefs: {}
  279. })
  280. }
  281. parseResults[parseResults.length - 1].topicIndex[ref] = topic;
  282. }
  283. };
  284. parseResults.forEach(parseResult => populateTopicsFromTree(parseResult.topics));
  285. let parseNextTopic = () => {
  286. if (topicsToParse.length) {
  287. parseTopic(topicsToParse.shift(), cssClassPrefix, conrefCallback).then(parseNextTopic).catch(reject);
  288. } else {
  289. resolve();
  290. }
  291. };
  292. parseNextTopic();
  293. });
  294. module.exports = {
  295. parseTopics: parseTopics,
  296. isHidden: isHidden
  297. };