ProjectRootDocManager.js 9.2 KB

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