ProjectRootDocManager.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. /* eslint-disable
  2. camelcase,
  3. node/handle-callback-err,
  4. max-len,
  5. no-unused-vars,
  6. no-useless-escape,
  7. */
  8. // TODO: This file was created by bulk-decaffeinate.
  9. // Fix any style issues and re-enable lint.
  10. /*
  11. * decaffeinate suggestions:
  12. * DS102: Remove unnecessary code created because of implicit returns
  13. * DS207: Consider shorter variations of null checks
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. let ProjectRootDocManager
  17. const ProjectEntityHandler = require('./ProjectEntityHandler')
  18. const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler')
  19. const ProjectGetter = require('./ProjectGetter')
  20. const DocumentHelper = require('../Documents/DocumentHelper')
  21. const Path = require('path')
  22. const fs = require('fs')
  23. const { promisify } = require('util')
  24. const async = require('async')
  25. const globby = require('globby')
  26. const _ = require('underscore')
  27. module.exports = ProjectRootDocManager = {
  28. setRootDocAutomatically(project_id, callback) {
  29. if (callback == null) {
  30. callback = function (error) {}
  31. }
  32. return ProjectEntityHandler.getAllDocs(project_id, function (error, docs) {
  33. if (error != null) {
  34. return callback(error)
  35. }
  36. const jobs = _.map(
  37. docs,
  38. (doc, path) =>
  39. function (cb) {
  40. if (
  41. ProjectEntityUpdateHandler.isPathValidForRootDoc(path) &&
  42. DocumentHelper.contentHasDocumentclass(doc.lines)
  43. ) {
  44. async.setImmediate(function () {
  45. cb(doc._id)
  46. })
  47. } else {
  48. async.setImmediate(function () {
  49. cb(null)
  50. })
  51. }
  52. }
  53. )
  54. return async.series(jobs, function (root_doc_id) {
  55. if (root_doc_id != null) {
  56. return ProjectEntityUpdateHandler.setRootDoc(
  57. project_id,
  58. root_doc_id,
  59. callback
  60. )
  61. } else {
  62. return callback()
  63. }
  64. })
  65. })
  66. },
  67. findRootDocFileFromDirectory(directoryPath, callback) {
  68. if (callback == null) {
  69. callback = function (error, path, content) {}
  70. }
  71. const filePathsPromise = globby(['**/*.{tex,Rtex}'], {
  72. cwd: directoryPath,
  73. followSymlinkedDirectories: false,
  74. onlyFiles: true,
  75. case: false
  76. })
  77. // the search order is such that we prefer files closer to the project root, then
  78. // we go by file size in ascending order, because people often have a main
  79. // file that just includes a bunch of other files; then we go by name, in
  80. // order to be deterministic
  81. filePathsPromise.then(
  82. unsortedFiles =>
  83. ProjectRootDocManager._sortFileList(
  84. unsortedFiles,
  85. directoryPath,
  86. function (err, files) {
  87. if (err != null) {
  88. return callback(err)
  89. }
  90. let doc = null
  91. return async.until(
  92. () => doc != null || files.length === 0,
  93. function (cb) {
  94. const file = files.shift()
  95. return fs.readFile(
  96. Path.join(directoryPath, file),
  97. 'utf8',
  98. function (error, content) {
  99. if (error != null) {
  100. return cb(error)
  101. }
  102. content = (content || '').replace(/\r/g, '')
  103. if (DocumentHelper.contentHasDocumentclass(content)) {
  104. doc = { path: file, content }
  105. }
  106. return cb(null)
  107. }
  108. )
  109. },
  110. err =>
  111. callback(
  112. err,
  113. doc != null ? doc.path : undefined,
  114. doc != null ? doc.content : undefined
  115. )
  116. )
  117. }
  118. ),
  119. err => callback(err)
  120. )
  121. // coffeescript's implicit-return mechanism returns filePathsPromise from this method, which confuses mocha
  122. return null
  123. },
  124. setRootDocFromName(project_id, rootDocName, callback) {
  125. if (callback == null) {
  126. callback = function (error) {}
  127. }
  128. return ProjectEntityHandler.getAllDocPathsFromProjectById(
  129. project_id,
  130. function (error, docPaths) {
  131. let doc_id, path
  132. if (error != null) {
  133. return callback(error)
  134. }
  135. // strip off leading and trailing quotes from rootDocName
  136. rootDocName = rootDocName.replace(/^\'|\'$/g, '')
  137. // prepend a slash for the root folder if not present
  138. if (rootDocName[0] !== '/') {
  139. rootDocName = `/${rootDocName}`
  140. }
  141. // find the root doc from the filename
  142. let root_doc_id = null
  143. for (doc_id in docPaths) {
  144. // docpaths have a leading / so allow matching "folder/filename" and "/folder/filename"
  145. path = docPaths[doc_id]
  146. if (path === rootDocName) {
  147. root_doc_id = doc_id
  148. }
  149. }
  150. // try a basename match if there was no match
  151. if (!root_doc_id) {
  152. for (doc_id in docPaths) {
  153. path = docPaths[doc_id]
  154. if (Path.basename(path) === Path.basename(rootDocName)) {
  155. root_doc_id = doc_id
  156. }
  157. }
  158. }
  159. // set the root doc id if we found a match
  160. if (root_doc_id != null) {
  161. return ProjectEntityUpdateHandler.setRootDoc(
  162. project_id,
  163. root_doc_id,
  164. callback
  165. )
  166. } else {
  167. return callback()
  168. }
  169. }
  170. )
  171. },
  172. ensureRootDocumentIsSet(project_id, callback) {
  173. if (callback == null) {
  174. callback = function (error) {}
  175. }
  176. return ProjectGetter.getProject(
  177. project_id,
  178. { rootDoc_id: 1 },
  179. function (error, project) {
  180. if (error != null) {
  181. return callback(error)
  182. }
  183. if (project == null) {
  184. return callback(new Error('project not found'))
  185. }
  186. if (project.rootDoc_id != null) {
  187. return callback()
  188. } else {
  189. return ProjectRootDocManager.setRootDocAutomatically(
  190. project_id,
  191. callback
  192. )
  193. }
  194. }
  195. )
  196. },
  197. ensureRootDocumentIsValid(project_id, callback) {
  198. if (callback == null) {
  199. callback = function (error) {}
  200. }
  201. return ProjectGetter.getProject(
  202. project_id,
  203. { rootDoc_id: 1 },
  204. function (error, project) {
  205. if (error != null) {
  206. return callback(error)
  207. }
  208. if (project == null) {
  209. return callback(new Error('project not found'))
  210. }
  211. if (project.rootDoc_id != null) {
  212. return ProjectEntityHandler.getAllDocPathsFromProjectById(
  213. project_id,
  214. function (error, docPaths) {
  215. if (error != null) {
  216. return callback(error)
  217. }
  218. let rootDocValid = false
  219. for (let doc_id in docPaths) {
  220. const _path = docPaths[doc_id]
  221. if (doc_id === project.rootDoc_id) {
  222. rootDocValid = true
  223. }
  224. }
  225. if (rootDocValid) {
  226. return callback()
  227. } else {
  228. return ProjectEntityUpdateHandler.unsetRootDoc(project_id, () =>
  229. ProjectRootDocManager.setRootDocAutomatically(
  230. project_id,
  231. callback
  232. )
  233. )
  234. }
  235. }
  236. )
  237. } else {
  238. return ProjectRootDocManager.setRootDocAutomatically(
  239. project_id,
  240. callback
  241. )
  242. }
  243. }
  244. )
  245. },
  246. _sortFileList(listToSort, rootDirectory, callback) {
  247. if (callback == null) {
  248. callback = function (error, result) {}
  249. }
  250. return async.mapLimit(
  251. listToSort,
  252. 5,
  253. (filePath, cb) =>
  254. fs.stat(Path.join(rootDirectory, filePath), function (err, stat) {
  255. if (err != null) {
  256. return cb(err)
  257. }
  258. return cb(null, {
  259. size: stat.size,
  260. path: filePath,
  261. elements: filePath.split(Path.sep).length,
  262. name: Path.basename(filePath)
  263. })
  264. }),
  265. function (err, files) {
  266. if (err != null) {
  267. return callback(err)
  268. }
  269. return callback(
  270. null,
  271. _.map(
  272. files.sort(ProjectRootDocManager._rootDocSort),
  273. file => file.path
  274. )
  275. )
  276. }
  277. )
  278. },
  279. _rootDocSort(a, b) {
  280. // sort first by folder depth
  281. if (a.elements !== b.elements) {
  282. return a.elements - b.elements
  283. }
  284. // ensure main.tex is at the start of each folder
  285. if (a.name === 'main.tex' && b.name !== 'main.tex') {
  286. return -1
  287. }
  288. if (a.name !== 'main.tex' && b.name === 'main.tex') {
  289. return 1
  290. }
  291. // prefer smaller files
  292. if (a.size !== b.size) {
  293. return a.size - b.size
  294. }
  295. // otherwise, use the full path name
  296. return a.path.localeCompare(b.path)
  297. }
  298. }
  299. const promises = {
  300. setRootDocAutomatically: promisify(
  301. ProjectRootDocManager.setRootDocAutomatically
  302. ),
  303. findRootDocFileFromDirectory: directoryPath =>
  304. new Promise((resolve, reject) => {
  305. ProjectRootDocManager.findRootDocFileFromDirectory(
  306. directoryPath,
  307. (error, path, content) => {
  308. if (error) {
  309. reject(error)
  310. } else {
  311. resolve({ path, content })
  312. }
  313. }
  314. )
  315. }),
  316. setRootDocFromName: promisify(ProjectRootDocManager.setRootDocFromName)
  317. }
  318. ProjectRootDocManager.promises = promises
  319. module.exports = ProjectRootDocManager