ProjectRootDocManager.js 8.7 KB

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