docXmlParser.js 11 KB

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