docXmlParser.js 11 KB

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