docXmlParser.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  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 (
  147. docElement
  148. .parent()
  149. .name()
  150. .toLowerCase() === 'row'
  151. ) {
  152. domElement = domElement.node('td');
  153. } else {
  154. console.log(
  155. '%s: Got "entry" element without a parent "row": %s in ref %s',
  156. LOG_NAME,
  157. docElement.toString(),
  158. topic.ref
  159. );
  160. return;
  161. }
  162. break;
  163. case 'xref':
  164. if (
  165. extractorUtils.hasAttributes(docElement, 'href') &&
  166. (!docElement.attr('scope') || docElement.attr('scope').value() !== 'external')
  167. ) {
  168. docElement.attr('href', makeAbsoluteRef(docElement.attr('href').value(), topic));
  169. }
  170. case 'image':
  171. case 'imagemap':
  172. case 'keyword':
  173. // These elements are dealt with later, we don't deep clone as there might be child elements to parse
  174. domElement = domElement.node(docElement.name());
  175. docElement.attrs().forEach(attr => {
  176. domElement.attr(attr.name(), attr.value());
  177. });
  178. break;
  179. case 'object':
  180. if (extractorUtils.hasAttributes(docElement, ['data', 'outputclass'])) {
  181. domElement = domElement.node('iframe');
  182. domElement.attr({
  183. class: cssClassPrefix + 'doc-iframe',
  184. src: docElement.attr('data').value()
  185. });
  186. if (extractorUtils.hasAttributes(docElement, 'width')) {
  187. domElement.attr({ width: docElement.attr('width').value() });
  188. }
  189. if (extractorUtils.hasAttributes(docElement, 'height')) {
  190. domElement.attr({ height: docElement.attr('height').value() });
  191. }
  192. } else {
  193. console.log(
  194. '%s: Got "object" element without data and outputclass: %s in ref %s',
  195. LOG_NAME,
  196. docElement.toString(),
  197. topic.ref
  198. );
  199. return;
  200. }
  201. break;
  202. case 'pre': // Enables better styling if div + class
  203. case 'cmdname':
  204. case 'codeph':
  205. case 'filepath':
  206. case 'lines':
  207. case 'option':
  208. case 'parmname':
  209. case 'ph':
  210. case 'systemoutput':
  211. case 'term':
  212. case 'userinput':
  213. case 'apiname':
  214. case 'varname':
  215. if (isHidden(docElement)) {
  216. return;
  217. }
  218. domElement = domElement.node('span');
  219. domElement.attr({ class: cssClassPrefix + 'doc-' + docElement.name() });
  220. break;
  221. case 'codeblock':
  222. case 'conbodydiv':
  223. case 'example':
  224. case 'fig':
  225. case 'menucascade':
  226. case 'msgblock':
  227. case 'note':
  228. case 'section':
  229. case 'sectiondiv':
  230. case 'title':
  231. case 'uicontrol':
  232. if (isHidden(docElement)) {
  233. return;
  234. }
  235. domElement = domElement.node('div');
  236. domElement.attr({ class: cssClassPrefix + 'doc-' + docElement.name() });
  237. if (docElement.name() === 'title' && activeFragment && !activeFragment.title) {
  238. activeFragment.title = domElement;
  239. }
  240. break;
  241. case 'text':
  242. if (docElement.text().trim()) {
  243. const firstInDiv = domElement.name() === 'div' && domElement.childNodes().length === 0;
  244. domElement = domElement.node('text');
  245. domElement.replace(
  246. firstInDiv ? docElement.text().replace(/^[\n\r]*/, '') : docElement.text()
  247. );
  248. }
  249. break;
  250. case 'abstract':
  251. case 'comment':
  252. case 'data':
  253. case 'draft-comment':
  254. case 'indexterm':
  255. case 'oxy_attributes':
  256. case 'oxy_comment_start':
  257. case 'oxy_comment_end':
  258. case 'oxy_delete':
  259. case 'oxy_insert_start':
  260. case 'oxy_insert_end':
  261. case 'prolog':
  262. case 'shortdesc':
  263. case 'titlealts':
  264. return;
  265. case undefined:
  266. if (/^<\!\[cdata.*/i.test(docElement.toString())) {
  267. if (docElement.text().trim()) {
  268. const firstInDiv = domElement.name() === 'div' && domElement.childNodes().length === 0;
  269. domElement = domElement.node('text');
  270. domElement.replace(
  271. firstInDiv ? docElement.text().replace(/^[\n\r]*/, '') : docElement.text()
  272. );
  273. }
  274. break;
  275. }
  276. default:
  277. console.log("%s: Can't handle node: %s in ref %s", LOG_NAME, docElement.name(), topic.ref);
  278. return;
  279. }
  280. if (isHidden(docElement)) {
  281. domElement.attr({ style: 'display:none;' });
  282. }
  283. if (extractorUtils.hasAttributes(docElement, 'id')) {
  284. const fragmentId = docElement.attr('id') && docElement.attr('id').value();
  285. const newFragment = new DocFragment(fragmentId, domElement);
  286. if (!extractorUtils.hasAttributes(domElement, 'id') && domElement.type() === 'element') {
  287. domElement.attr({ id: fragmentId });
  288. }
  289. if (!topic.fragment) {
  290. topic.fragment = newFragment;
  291. } else {
  292. activeFragment.children.push(newFragment);
  293. }
  294. activeFragment = newFragment;
  295. }
  296. if (
  297. extractorUtils.hasAttributes(docElement, 'conref') &&
  298. !extractorUtils.hasAttributes(domElement, 'conref')
  299. ) {
  300. domElement.attr('conref', docElement.attr('conref').value());
  301. }
  302. docElement
  303. .childNodes()
  304. .forEach(childNode =>
  305. parseDocElement(childNode, domElement, cssClassPrefix, topic, activeFragment, conrefCallback)
  306. );
  307. };
  308. /**
  309. * Parses all the topic xml files and sets the intermediary DOM on the topic, after this linkage is required to insert
  310. * any conrefs or keywords etc. that are only known after parsing all the topics.
  311. *
  312. * @param parseResults
  313. * @param cssClassPrefix
  314. * @return {Promise}
  315. */
  316. const parseTopics = (parseResults, cssClassPrefix) =>
  317. new Promise((resolve, reject) => {
  318. const topicIndex = {};
  319. const topicsToParse = [];
  320. const populateTopicsFromTree = topics => {
  321. topics.forEach(topic => {
  322. topicsToParse.push(topic);
  323. topicIndex[topic.ref] = true;
  324. populateTopicsFromTree(topic.children);
  325. });
  326. };
  327. // Topics might be referenced from within .xml files thar are not part of the ditamap, we add them here to make
  328. // sure they're parsed
  329. const conrefCallback = (sourceTopic, ref) => {
  330. if (!topicIndex[ref]) {
  331. const topic = new Topic(sourceTopic.docRootPath, ref);
  332. topicIndex[ref] = true;
  333. topicsToParse.push(topic);
  334. if (parseResults.length < 2) {
  335. // We add additional topics to any ditamap parseresults except the first one, this prevents
  336. // them from being part of the tree.
  337. parseResults.push({
  338. topics: [],
  339. topicIndex: {},
  340. keyDefs: {}
  341. });
  342. }
  343. parseResults[parseResults.length - 1].topicIndex[ref] = topic;
  344. }
  345. };
  346. parseResults.forEach(parseResult => populateTopicsFromTree(parseResult.topics));
  347. const parseNextTopic = () => {
  348. if (topicsToParse.length) {
  349. parseTopic(topicsToParse.shift(), cssClassPrefix, conrefCallback)
  350. .then(parseNextTopic)
  351. .catch(reject);
  352. } else {
  353. resolve();
  354. }
  355. };
  356. parseNextTopic();
  357. });
  358. module.exports = {
  359. parseTopics: parseTopics,
  360. isHidden: isHidden
  361. };
  362. /* eslint-enable no-restricted-syntax */