docXmlParser.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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':
  73. if (extractorUtils.hasAttributes(docElement, 'id')) {
  74. let id = docElement.attr('id') && docElement.attr('id').value();
  75. // Move id attribute to first child element
  76. for (let node of docElement.childNodes()) {
  77. if (node.type() === 'element') {
  78. node.attr({'id': id});
  79. break;
  80. }
  81. }
  82. docElement.attr('id').remove();
  83. }
  84. // skip creating corresponding DOM element
  85. break;
  86. case 'alt':
  87. case 'area':
  88. case 'b':
  89. case 'cite':
  90. case 'coords':
  91. case 'dd':
  92. case 'dl':
  93. case 'dt':
  94. case 'fn':
  95. case 'i':
  96. case 'li':
  97. case 'ol':
  98. case 'p':
  99. case 'shape':
  100. case 'q':
  101. case 'sup':
  102. case 'table':
  103. case 'tbody':
  104. case 'thead':
  105. case 'tt':
  106. case 'u':
  107. case 'ul':
  108. if (isHidden(docElement)) {
  109. return;
  110. }
  111. domElement = domElement.node(docElement.name());
  112. if (extractorUtils.hasAttributes(docElement, 'conref')) {
  113. domElement.attr('conref', docElement.attr('conref').value());
  114. }
  115. break;
  116. case 'sthead':
  117. domElement = domElement.node('tr');
  118. domElement.attr({ 'class': cssClassPrefix + 'doc-sthead' });
  119. break;
  120. case 'stentry':
  121. domElement = domElement.node('td');
  122. break;
  123. case 'simpletable':
  124. domElement = domElement.node('table');
  125. break;
  126. case 'strow':
  127. case 'row':
  128. domElement = domElement.node('tr');
  129. break;
  130. case 'entry':
  131. if (docElement.parent().name().toLowerCase() === 'row') {
  132. domElement = domElement.node('td');
  133. } else {
  134. console.log('%s: Got "entry" element without a parent "row": %s in ref %s', LOG_NAME, docElement.toString(), topic.ref);
  135. return;
  136. }
  137. break;
  138. case 'xref':
  139. if (extractorUtils.hasAttributes(docElement, 'href') && (!docElement.attr('scope') || docElement.attr('scope').value() !== 'external')) {
  140. docElement.attr('href', makeAbsoluteRef(docElement.attr('href').value(), topic));
  141. }
  142. case 'image':
  143. case 'imagemap':
  144. case 'keyword':
  145. // These elements are dealt with later, we don't deep clone as there might be child elements to parse
  146. domElement = domElement.node(docElement.name());
  147. docElement.attrs().forEach(attr => {
  148. domElement.attr(attr.name(), attr.value())
  149. });
  150. break;
  151. case 'object':
  152. if (extractorUtils.hasAttributes(docElement, ['data', 'outputclass'])) {
  153. domElement = domElement.node('iframe');
  154. domElement.attr({ 'class': cssClassPrefix + 'doc-iframe', 'src': docElement.attr('data').value() });
  155. if (extractorUtils.hasAttributes(docElement, 'width')) {
  156. domElement.attr({ 'width': docElement.attr('width').value() });
  157. }
  158. if (extractorUtils.hasAttributes(docElement, 'height')) {
  159. domElement.attr({ 'height': docElement.attr('height').value() });
  160. }
  161. } else {
  162. console.log('%s: Got "object" element without data and outputclass: %s in ref %s', LOG_NAME, docElement.toString(), topic.ref);
  163. return;
  164. }
  165. break;
  166. case 'pre': // Enables better styling if div + class
  167. case 'cmdname':
  168. case 'codeph':
  169. case 'filepath':
  170. case 'lines':
  171. case 'option':
  172. case 'parmname':
  173. case 'ph':
  174. case 'systemoutput':
  175. case 'term':
  176. case 'userinput':
  177. case 'apiname':
  178. case 'varname':
  179. if (isHidden(docElement)) {
  180. return;
  181. }
  182. domElement = domElement.node('span');
  183. domElement.attr({ 'class': cssClassPrefix + 'doc-' + docElement.name() });
  184. break;
  185. case 'codeblock':
  186. case 'conbodydiv':
  187. case 'example':
  188. case 'fig':
  189. case 'menucascade':
  190. case 'msgblock':
  191. case 'note':
  192. case 'section':
  193. case 'sectiondiv':
  194. case 'title':
  195. case 'uicontrol':
  196. if (isHidden(docElement)) {
  197. return;
  198. }
  199. domElement = domElement.node('div');
  200. domElement.attr({ 'class': cssClassPrefix + 'doc-' + docElement.name() });
  201. if (docElement.name() === 'title' && activeFragment && !activeFragment.title) {
  202. activeFragment.title = domElement;
  203. }
  204. break;
  205. case 'text':
  206. if (docElement.text().trim()) {
  207. let firstInDiv = domElement.name() === 'div' && domElement.childNodes().length === 0;
  208. domElement = domElement.node('text');
  209. domElement.replace(firstInDiv ? docElement.text().replace(/^[\n\r]*/, '') : docElement.text());
  210. }
  211. break;
  212. case 'abstract':
  213. case 'comment':
  214. case 'data':
  215. case 'draft-comment':
  216. case 'indexterm':
  217. case 'oxy_attributes':
  218. case 'oxy_comment_start':
  219. case 'oxy_comment_end':
  220. case 'oxy_delete':
  221. case 'oxy_insert_start':
  222. case 'oxy_insert_end':
  223. case 'prolog':
  224. case 'shortdesc':
  225. case 'titlealts':
  226. return;
  227. case undefined:
  228. if (/^<\!\[cdata.*/i.test(docElement.toString())) {
  229. if (docElement.text().trim()) {
  230. let firstInDiv = domElement.name() === 'div' && domElement.childNodes().length === 0;
  231. domElement = domElement.node('text');
  232. domElement.replace(firstInDiv ? docElement.text().replace(/^[\n\r]*/, '') : docElement.text());
  233. }
  234. break;
  235. }
  236. default:
  237. console.log('%s: Can\'t handle node: %s in ref %s', LOG_NAME, docElement.name(), topic.ref);
  238. return;
  239. }
  240. if (isHidden(docElement)) {
  241. domElement.attr({ 'style': 'display:none;' });
  242. }
  243. if (extractorUtils.hasAttributes(docElement, 'id')) {
  244. let fragmentId = docElement.attr('id') && docElement.attr('id').value();
  245. let newFragment = new DocFragment(fragmentId, domElement);
  246. if (!extractorUtils.hasAttributes(domElement, 'id') && domElement.type() === 'element') {
  247. domElement.attr({'id': fragmentId});
  248. }
  249. if (!topic.fragment) {
  250. topic.fragment = newFragment;
  251. } else {
  252. activeFragment.children.push(newFragment);
  253. }
  254. activeFragment = newFragment;
  255. }
  256. if (extractorUtils.hasAttributes(docElement, 'conref') && !extractorUtils.hasAttributes(domElement, 'conref')) {
  257. domElement.attr('conref', docElement.attr('conref').value());
  258. }
  259. docElement.childNodes().forEach(childNode => parseDocElement(childNode, domElement, cssClassPrefix, topic, activeFragment, conrefCallback));
  260. };
  261. /**
  262. * Parses all the topic xml files and sets the intermediary DOM on the topic, after this linkage is required to insert
  263. * any conrefs or keywords etc. that are only known after parsing all the topics.
  264. *
  265. * @param parseResults
  266. * @param cssClassPrefix
  267. * @return {Promise}
  268. */
  269. const parseTopics = (parseResults, cssClassPrefix) => new Promise((resolve, reject) => {
  270. let topicIndex = {};
  271. let topicsToParse = [];
  272. let populateTopicsFromTree = topics => {
  273. topics.forEach(topic => {
  274. topicsToParse.push(topic);
  275. topicIndex[topic.ref] = true;
  276. populateTopicsFromTree(topic.children);
  277. })
  278. };
  279. // Topics might be referenced from within .xml files thar are not part of the ditamap, we add them here to make
  280. // sure they're parsed
  281. let conrefCallback = (sourceTopic, ref) => {
  282. if (!topicIndex[ref]) {
  283. let topic = new Topic(sourceTopic.docRootPath, ref);
  284. topicIndex[ref] = true;
  285. topicsToParse.push(topic);
  286. if (parseResults.length < 2) {
  287. // We add additional topics to any ditamap parseresults except the first one, this prevents
  288. // them from being part of the tree.
  289. parseResults.push({
  290. topics: [],
  291. topicIndex: {},
  292. keyDefs: {}
  293. })
  294. }
  295. parseResults[parseResults.length - 1].topicIndex[ref] = topic;
  296. }
  297. };
  298. parseResults.forEach(parseResult => populateTopicsFromTree(parseResult.topics));
  299. let parseNextTopic = () => {
  300. if (topicsToParse.length) {
  301. parseTopic(topicsToParse.shift(), cssClassPrefix, conrefCallback).then(parseNextTopic).catch(reject);
  302. } else {
  303. resolve();
  304. }
  305. };
  306. parseNextTopic();
  307. });
  308. module.exports = {
  309. parseTopics: parseTopics,
  310. isHidden: isHidden
  311. };