<commit_before>/** @babel */

import elasticlunr from 'elasticlunr'
import fs from 'fs-plus'
import path from 'path'
import matter from 'gray-matter'
import * as NotesFs from './notes-fs'

let {EventEmitter} = require('events')
const {watchPath} = require('atom')

/**
  * @typedef DocumentItem
  * @type {Object}
  * @property {string} title - Title of the note.
  * @property {string} fileName - Name of the file where note is stored.
  * @property {string} filePath - Path to the file where note is stored.
  * @property {string} body - Body of the note.
  * @property {Array} keywords - Keywords of the note.
  * @property {Array} abstract - Summary of the note.
  * @property {Date} modifiedAt - Date and time of last modification to the note.
  */

/**
 * Builds a document object to hold state about a note document.
 * @param  {String} filePath Full path to the document.
 * @return {DocumentItem}
 */
function createDocument (filePath) {
  if (!fs.existsSync(filePath)) { return false }

  // we can't just check isFile() b/c we want to support symlinks to files
  let fileStats = fs.statSync(filePath)
  if (fileStats.isDirectory() ||
      fileStats.isBlockDevice() ||
      fileStats.isCharacterDevice() ||
      fileStats.isFIFO() ||
      fileStats.isSocket()) { return false }

  let fileName = path.basename(filePath)
  let title = path.basename(fileName, path.extname(fileName))
  let keywords = []
  let abstract = null

  let body
  try {
    body = fs.readFileSync(filePath, {encoding: 'utf8'})
  } catch (_) {
    // if we fail to read the file for whatever reason, let's ignore the file
    return false
  }

  let meta
  try {
    meta = matter(body).data
  } catch (_) {
    // If YAML parsing fails, that's fine. We just won't have meta data.
  }

  if (meta !== undefined && meta.keywords !== undefined) {
    keywords = meta.keywords
  }

  if (meta !== undefined && meta.abstract !== undefined) {
    abstract = meta.abstract
  }

  try {
    return {
      filePath: filePath,
      fileName: fileName,
      title: title,
      modifiedAt: fileStats.mtime,
      body: body,
      keywords: keywords,
      abstract: abstract
    }
  } catch (_) {
    return {}
  }
}

async function setupFileWatcher (directoryPath) {
  const watchedDirectory = fs.normalize(directoryPath)
  fs.listTreeSync(watchedDirectory).forEach(path => {
    if (!fs.isDirectorySync(path)) {
      if (NotesFs.isNote(path)) {
        this.addDocument(createDocument(path))
      }
    }
  })
  return watchPath(watchedDirectory, {}, events => {
    for (const event of events) {
      if (event.action === 'created') {
        this.addDocument(createDocument(event.path))
      } else if (event.action === 'modified') {
        this.updateDocument(createDocument(event.path))
      } else if (event.action === 'deleted') {
        this.removeDocument(this._documents[event.path])
      } else if (event.action === 'renamed') {
        this.removeDocument(this._documents[event.oldPath])
        this.addDocument(createDocument(event.path))
      }
    }
  })
}

class NotesStore extends EventEmitter {
  /**
   * Document storage for notes.
   * @param {String}   directoryPath Full path to where notes are stored.
   * @param {String[]} extensions    File extensions of notes.
   * @param {Object}   [index=null]  Serialized elasticlunr index to load.
   */
  constructor (directoryPath, extensions, index = null) {
    super()
    this.directoryPath = directoryPath
    this.extensions = extensions
    this.index = index
    this._documents = {}
  }

  /**
   * Initialize document storage by creating the document index.
   * Emits "ready" when index is ready.
   */
  initialize () {
    if (this.index) {
      console.log('atom-notes: loading...')
      this.loaded = true
      this.index = elasticlunr.Index.load(this.index)
    } else {
      console.log('atom-notes: indexing...')
      this.loaded = false
      this.index = elasticlunr(function () {
        this.addField('title')
        this.addField('body')
        this.addField('keywords')
        this.addField('abstract')
      })
    }

    this.watcher = setupFileWatcher.bind(this)(this.directoryPath)
    this.emit('ready')
  }

  /**
   * Adds the given document to our index.
   * @param {DocumentItem} doc
   */
  addDocument (doc) {
    this._documents[doc.filePath] = doc
    let data = {
      id: doc.filePath,
      title: doc.title,
      body: doc.body,
      abstract: doc.abstract
    }
    if (doc.keywords && Array.isArray(doc.keywords)) {
      data.keywords = doc.keywords.join(', ')
    }
    this.index.addDoc(data)
    this.emit('added', doc)
  }

  /**
   * Updates the given document, identified by filePath, in our index.
   * @param {DocumentItem} doc
   */
  updateDocument (doc) {
    this._documents[doc.filePath] = doc
    let data = {
      id: doc.filePath,
      title: doc.title,
      body: doc.body,
      abstract: doc.abstract
    }
    if (doc.keywords && Array.isArray(doc.keywords)) {
      data.keywords = doc.keywords.join(', ')
    }
    this.index.updateDoc(data)
    this.emit('updated', doc)
  }

  /**
   * Removes the given document, identified by filePath, from our index.
   * @param {DocumentItem} doc
   */
  removeDocument (doc) {
    if (doc === undefined) { return false }
    delete this._documents[doc.filePath]
    let data = {
      id: doc.filePath,
      title: doc.title,
      body: doc.body,
      abstract: doc.abstract
    }
    if (doc.keywords && Array.isArray(doc.keywords)) {
      data.keywords = doc.keywords.join(', ')
    }
    this.index.removeDoc(data)<commit_msg>Adds catches for a possible error state.<commit_after>/** @babel */

import elasticlunr from 'elasticlunr'
import fs from 'fs-plus'
import path from 'path'
import matter from 'gray-matter'
import * as NotesFs from './notes-fs'

let {EventEmitter} = require('events')
const {watchPath} = require('atom')

function isEmpty (obj) {
  return Object.keys(obj).length === 0 && obj.constructor === Object
}

/**
  * @typedef DocumentItem
  * @type {Object}
  * @property {string} title - Title of the note.
  * @property {string} fileName - Name of the file where note is stored.
  * @property {string} filePath - Path to the file where note is stored.
  * @property {string} body - Body of the note.
  * @property {Array} keywords - Keywords of the note.
  * @property {Array} abstract - Summary of the note.
  * @property {Date} modifiedAt - Date and time of last modification to the note.
  */

/**
 * Builds a document object to hold state about a note document.
 * @param  {String} filePath Full path to the document.
 * @return {DocumentItem}
 */
function createDocument (filePath) {
  if (!fs.existsSync(filePath)) { return false }

  // we can't just check isFile() b/c we want to support symlinks to files
  let fileStats = fs.statSync(filePath)
  if (fileStats.isDirectory() ||
      fileStats.isBlockDevice() ||
      fileStats.isCharacterDevice() ||
      fileStats.isFIFO() ||
      fileStats.isSocket()) { return false }

  let fileName = path.basename(filePath)
  let title = path.basename(fileName, path.extname(fileName))
  let keywords = []
  let abstract = null

  let body
  try {
    body = fs.readFileSync(filePath, {encoding: 'utf8'})
  } catch (_) {
    // if we fail to read the file for whatever reason, let's ignore the file
    return false
  }

  let meta
  try {
    meta = matter(body).data
  } catch (_) {
    // If YAML parsing fails, that's fine. We just won't have meta data.
  }

  if (meta !== undefined && meta.keywords !== undefined) {
    keywords = meta.keywords
  }

  if (meta !== undefined && meta.abstract !== undefined) {
    abstract = meta.abstract
  }

  try {
    return {
      filePath: filePath,
      fileName: fileName,
      title: title,
      modifiedAt: fileStats.mtime,
      body: body,
      keywords: keywords,
      abstract: abstract
    }
  } catch (_) {
    return {}
  }
}

async function setupFileWatcher (directoryPath) {
  const watchedDirectory = fs.normalize(directoryPath)
  fs.listTreeSync(watchedDirectory).forEach(path => {
    if (!fs.isDirectorySync(path)) {
      if (NotesFs.isNote(path)) {
        this.addDocument(createDocument(path))
      }
    }
  })
  return watchPath(watchedDirectory, {}, events => {
    for (const event of events) {
      if (event.action === 'created') {
        this.addDocument(createDocument(event.path))
      } else if (event.action === 'modified') {
        this.updateDocument(createDocument(event.path))
      } else if (event.action === 'deleted') {
        this.removeDocument(this._documents[event.path])
      } else if (event.action === 'renamed') {
        this.removeDocument(this._documents[event.oldPath])
        this.addDocument(createDocument(event.path))
      }
    }
  })
}

class NotesStore extends EventEmitter {
  /**
   * Document storage for notes.
   * @param {String}   directoryPath Full path to where notes are stored.
   * @param {String[]} extensions    File extensions of notes.
   * @param {Object}   [index=null]  Serialized elasticlunr index to load.
   */
  constructor (directoryPath, extensions, index = null) {
    super()
    this.directoryPath = directoryPath
    this.extensions = extensions
    this.index = index
    this._documents = {}
  }

  /**
   * Initialize document storage by creating the document index.
   * Emits "ready" when index is ready.
   */
  initialize () {
    if (this.index) {
      console.log('atom-notes: loading...')
      this.loaded = true
      this.index = elasticlunr.Index.load(this.index)
    } else {
      console.log('atom-notes: indexing...')
      this.loaded = false
      this.index = elasticlunr(function () {
        this.addField('title')
        this.addField('body')
        this.addField('keywords')
        this.addField('abstract')
      })
    }

    this.watcher = setupFileWatcher.bind(this)(this.directoryPath)
    this.emit('ready')
  }

  /**
   * Adds the given document to our index.
   * @param {DocumentItem} doc
   */
  addDocument (doc) {
    if (isEmpty(doc)) return
    this._documents[doc.filePath] = doc
    let data = {
      id: doc.filePath,
      title: doc.title,
      body: doc.body,
      abstract: doc.abstract
    }
    if (doc.keywords && Array.isArray(doc.keywords)) {
      data.keywords = doc.keywords.join(', ')
    }
    this.index.addDoc(data)
    this.emit('added', doc)
  }

  /**
   * Updates the given document, identified by filePath, in our index.
   * @param {DocumentItem} doc
   */
  updateDocument (doc) {
    if (isEmpty(doc)) return
    this._documents[doc.filePath] = doc
    let data = {
      id: doc.filePath,
      title: doc.title,
      body: doc.body,
      abstract: doc.abstract
    }
    if (doc.keywords && Array.isArray(doc.keywords)) {
      data.keywords = doc.keywords.join(', ')
    }
    this.index.updateDoc(data)
    this.emit('updated', doc)
  }

  /**
   * Removes the given document, identified by filePath, from our index.
   * @param {DocumentItem} doc
   */
  removeDocument (doc) {
    if (isEmpty(doc)) return false
    if (doc === undefined) return false
    delete this._documents[doc.filePath]
    let data = {
      id: doc.filePath,
      title: doc.title,
      body: doc.body,
      abstract: doc.abstract
    }
    if (doc.keywords && Array.isArray(doc.keywords)) {
      data.keywords = doc.keywords.join(', ')
    }
    this.index.removeDoc(data)
{'fraction_non_alphanumeric': 0.12898715371843048, 'fraction_numerical': 0.00026216901162282616, 'mean_word_length': 2.7533617579534275, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '66980', 'n_tokens_mistral': 3577, 'n_tokens_neox': 3495, 'n_words': 947}