├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── doc └── img │ └── atom-gitbook.gif ├── keymaps └── atom-gitbook.cson ├── lib ├── atom-gitbook-view.coffee ├── atom-gitbook.coffee ├── chapter-view.coffee ├── helper │ ├── asdoc-summary.coffee │ ├── include-parser.coffee │ ├── markdown-summary.coffee │ ├── path-helper.coffee │ ├── retext-summary.coffee │ └── summary-parser.coffee ├── main.coffee └── nav-pane.coffee ├── menus └── atom-gitbook.cson ├── package.json ├── spec ├── atom-gitbook-spec.coffee ├── atom-gitbook-summary-parser-spec.coffee └── atom-gitbook-view-spec.coffee └── styles └── atom-gitbook.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | summary.md 5 | .idea 6 | *.iml 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 - First Release 2 | * Every feature added 3 | * Every bug fixed 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atom-gitbook package 2 | 3 | Plugin provides a Tree view for Gitbook Summary files, along with functionality that entails. 4 | 5 | ## Features 6 | 7 | * Preview Table of Contents from Summary.md 8 | * Auto-toggle Markdown Preview if installed 9 | * Reorder Chapters from the Table of Contents 10 | * Add existing files to the ToC from the tree view 11 | * Add and remove chapters 12 | * Create underlying Markdown file if it doesn't exist 13 | 14 | ![Demo](doc/img/atom-gitbook.gif) 15 | 16 | ## Developer instructions. 17 | 18 | ### Clone the repo 19 | 20 | ```bash 21 | git clone git@github.com:cthos/atom-gitbook.git 22 | ``` 23 | 24 | ### Link it to your gitbook directory 25 | 26 | ```bash 27 | ln -s /path/to/atom-gitbook ~/.atom/packages/atom-gitbook 28 | ``` 29 | 30 | ### NPM Install 31 | 32 | ```bash 33 | cd atom-gitbook 34 | apm install 35 | ``` 36 | 37 | It might also be necessary to `apm rebuild` from time-to-time as atom gets updated. 38 | 39 | #### Problems on Mac 40 | 41 | I've been seeing an issue where the package will not rebuild unless you do an `npm install` using node `0.10.35`. I've tested this with `n` to manage the versions, but `nvm` should also work... I think. 42 | 43 | More info here: https://www.alextheward.com/blog/apm-rebuild/ -------------------------------------------------------------------------------- /doc/img/atom-gitbook.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cthos/atom-gitbook/72955a1b002c63fe49e91d26600cf59c028b4a81/doc/img/atom-gitbook.gif -------------------------------------------------------------------------------- /keymaps/atom-gitbook.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/behind-atom-keymaps-in-depth 10 | 'atom-workspace': 11 | 'ctrl-alt-g': 'atom-gitbook:toggle' 12 | 'ctrl-alt-t': 'atom-gitbook:force-reload-toc' 13 | -------------------------------------------------------------------------------- /lib/atom-gitbook-view.coffee: -------------------------------------------------------------------------------- 1 | NavPane = require './nav-pane' 2 | fs = require 'fs-plus' 3 | path = require 'path' 4 | 5 | module.exports = 6 | class AtomGitbookView 7 | constructor: (serializedState) -> 8 | 9 | getNavPane: -> 10 | if not @navPane? 11 | @navPane = new NavPane 12 | @navPane 13 | 14 | show: -> 15 | @navPanel = atom.workspace.addLeftPanel item: @getNavPane() 16 | 17 | hide: -> 18 | @navPanel.destroy() 19 | 20 | refresh: (reloadFile = false, clearFile = false) -> 21 | # TODO: This chain is messy 22 | @navPane.refresh(reloadFile, clearFile) if @navPane 23 | 24 | organizeSummary: -> 25 | @getNavPane().getParser().organizeFilesFromTree() 26 | 27 | deleteChapter: -> 28 | atom.confirm 29 | message: "Are you sure you want to remove this chapter?" 30 | buttons: 31 | 'Yes': => 32 | # This can probably be refactored a bit 33 | removeFilesOnMenuDelete = atom.config.get('atom-gitbook.removeFilesOnMenuDelete') 34 | if removeFilesOnMenuDelete == 'Ask' 35 | atom.confirm 36 | message: "Would you like to remove the underlying file?" 37 | buttons: 38 | 'Yes': => @navPane.removeSelectedEntries(true) 39 | 'No' : => @navPane.removeSelectedEntries() 40 | 41 | else if removeFilesOnMenuDelete == 'Yes' 42 | @navPane.removeSelectedEntries(true) 43 | else 44 | @navPane.removeSelectedEntries() 45 | 'No': -> null 46 | 47 | 48 | # Returns an object that can be retrieved when package is activated 49 | serialize: -> 50 | 51 | # Tear down any state and detach 52 | destroy: -> 53 | @navPane.destroy() 54 | @element.remove() 55 | 56 | getElement: -> 57 | @element 58 | -------------------------------------------------------------------------------- /lib/atom-gitbook.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | fs = require 'fs-plus' 3 | path = require 'path' 4 | 5 | module.exports = 6 | class AtomGitbook 7 | 8 | openEditorFile: (filename, name) -> 9 | curPath = atom.project.getPaths()[0] 10 | 11 | filepath = path.join(curPath, filename) 12 | 13 | if fs.existsSync(filepath) 14 | atom.workspace.open(filepath).then => 15 | # TODO: Helper Class? 16 | editorElement = atom.views.getView(atom.workspace.getActiveTextEditor()) 17 | if atom.config.get('atom-gitbook.autoOpenMarkdownPreview') 18 | atom.commands.dispatch(editorElement, 'markdown-preview:toggle') 19 | else 20 | atom.confirm 21 | message: "Underlying File does not exist. Create it?" 22 | buttons: 23 | 'Okay': => 24 | # TODO Async? 25 | fs.writeFileSync(filepath, '# ' + name) 26 | atom.workspace.open(filepath).then => 27 | editorElement = atom.views.getView(atom.workspace.getActiveTextEditor()) 28 | atom.commands.dispatch(editorElement, 'markdown-preview:toggle') 29 | 'Cancel': -> null 30 | -------------------------------------------------------------------------------- /lib/chapter-view.coffee: -------------------------------------------------------------------------------- 1 | {$, TextEditorView, View} = require 'atom-space-pen-views' 2 | {Emitter} = require 'atom' 3 | slug = require 'slug' 4 | fs = require 'fs-plus' 5 | path = require 'path' 6 | 7 | module.exports = 8 | class NewChapterView extends View 9 | @content: -> 10 | @div class: 'new-chapter-modal', => 11 | @label 'Please enter the new Chapter Name' 12 | @subview 'miniEditor', new TextEditorView(mini: true) 13 | 14 | initialize: -> 15 | @emitter = new Emitter 16 | atom.commands.add @element, 17 | # Core confirm is emitted on "enter" for the mini TextEditorView 18 | 'core:confirm': => @onConfirm(@miniEditor.getText()) 19 | # Cancel is escape 20 | 'core:cancel': => @cancel() 21 | @miniEditor.on 'blur', => @close() 22 | 23 | onFileCreated: (callback) -> 24 | @emitter.on 'file-created', callback 25 | 26 | attach: -> 27 | @panel = atom.workspace.addModalPanel(item: this) 28 | @miniEditor.focus() 29 | @miniEditor.getModel().scrollToCursorPosition() 30 | 31 | onConfirm: -> 32 | # TODO: Handle sub-chapters 33 | txt = @miniEditor.getText() 34 | filename = slug(txt, {replacement: "_", lower: true}) + '.md' 35 | 36 | wsPath = atom.project.getPaths()[0] 37 | fullpath = path.join(wsPath, filename) 38 | 39 | if not fs.existsSync(fullpath) 40 | ## TODO: Async? 41 | fs.writeFileSync(fullpath, '# ' + txt) 42 | 43 | atom.workspace.open(fullpath).then => 44 | # TODO: Helper Class? 45 | editorElement = atom.views.getView(atom.workspace.getActiveTextEditor()) 46 | atom.commands.dispatch(editorElement, 'markdown-preview:toggle') 47 | 48 | # TODO Make this a singleton? 49 | Parser = require './helper/summary-parser' 50 | parse = Parser.getInstance(wsPath) 51 | 52 | parse.addSection(txt, filename) 53 | parse.generateFileFromTree() 54 | @emitter.emit 'file-created', filename 55 | 56 | @close() 57 | 58 | cancel: -> 59 | @close() 60 | 61 | close: -> 62 | depPanel = @panel 63 | @panel = null 64 | depPanel?.destroy() 65 | -------------------------------------------------------------------------------- /lib/helper/asdoc-summary.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | class AsdocSummary 3 | @formatTree: (tree) -> 4 | lines = ["= Summary"] 5 | for ele in tree 6 | continue if ele.name == 'Introduction' 7 | line = ". " 8 | if ele.file 9 | line = line + "link:#{ele.file}" 10 | line = line + "[#{ele.name}]" 11 | if ele.indent > 0 12 | dotindent = Math.floor ele.indent / 2 13 | for i in [1..dotindent] 14 | line = "." + line 15 | lines.push(line) 16 | 17 | return lines.join("\n") -------------------------------------------------------------------------------- /lib/helper/include-parser.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | fs = require 'fs-plus' 3 | MDRenderer = require path.join(atom.packages.resolvePackagePath('markdown-preview'), 'lib', 'renderer') 4 | 5 | module.exports = 6 | class IncludeParser 7 | 8 | @parseIncludesInText : (text, currentFileDir) -> 9 | re = new RegExp /(?:\)?\{\% include ['"]([^\%]+)["'] %\}(?:\<\/p\>)?/gi 10 | 11 | while (arr = re.exec(text)) != null 12 | continue unless filename = arr[1] 13 | 14 | includePath = path.join(currentFileDir, filename) 15 | 16 | continue unless fs.existsSync(includePath) 17 | incText = fs.readFileSync(includePath, 'utf-8') 18 | incText = IncludeParser.parseIncludesInText(incText, includePath) if incText 19 | 20 | text = text.replace(arr[0], incText) 21 | text 22 | 23 | @rerenderMarkdown : (text, pathToCurrentFile) -> 24 | promise = new Promise (resolve, reject) -> 25 | MDRenderer.toHTML(text, pathToCurrentFile, null, (error, html) -> 26 | resolve(html) 27 | ) 28 | return promise 29 | -------------------------------------------------------------------------------- /lib/helper/markdown-summary.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | class MarkdownSummary 3 | @formatTree: (tree) -> 4 | lines = ["# Summary"] 5 | for ele in tree 6 | continue if ele.name == 'Introduction' 7 | line = "* [#{ele.name}]" 8 | if ele.file 9 | line = line + "(#{ele.file})" 10 | if ele.indent > 0 11 | for i in [1..ele.indent] 12 | line = " " + line 13 | lines.push(line) 14 | 15 | return lines.join("\n") -------------------------------------------------------------------------------- /lib/helper/path-helper.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | _ = require 'lodash' 3 | fs = require 'fs' 4 | 5 | module.exports = 6 | class PathHelper 7 | @possibleExtensions = { 8 | 'markdown' : [".md", ".markdown", ".mdown"], 9 | 'asciidoc' : [".asdoc", ".asciidoc", ".adoc"], 10 | 'retext' : [".rst"] 11 | } 12 | 13 | @findExistingSummaryPath: (basePath) -> 14 | possibleExtensions = _.flatten(_.values(PathHelper.possibleExtensions)); 15 | 16 | for ext in possibleExtensions 17 | summaryName = 'summary' + ext 18 | summaryPath = path.join(basePath, summaryName) 19 | return summaryPath if fs.existsSync(summaryPath) -------------------------------------------------------------------------------- /lib/helper/retext-summary.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | class RetextSummary 3 | @formatTree: (tree) -> 4 | lines = ["Summary", "========="] 5 | for ele in tree 6 | line = "- `#{ele.name}" 7 | if ele.file 8 | line = line + " <#{ele.file})>" 9 | line = line + "`" 10 | if ele.indent > 0 11 | for i in [1..ele.indent] 12 | line = " " + line 13 | lines.push(line) 14 | 15 | return lines.join("\n") -------------------------------------------------------------------------------- /lib/helper/summary-parser.coffee: -------------------------------------------------------------------------------- 1 | {Emitter} = require 'atom' 2 | path = require 'path' 3 | fs = require 'fs-plus' 4 | slug = require 'slug' 5 | _ = require 'lodash' 6 | gitbookparse = require 'gitbook-parsers' 7 | PathHelper = require './path-helper' 8 | 9 | module.exports = 10 | class SummaryParser 11 | @instances = {} 12 | 13 | @getInstance: (directory) -> 14 | @instances[directory] ?= new SummaryParser(directory) 15 | 16 | constructor: (directory) -> 17 | @emitter = new Emitter 18 | @directory = directory 19 | 20 | @loadFromFile(@directory) 21 | 22 | clearFileCache: -> 23 | @lastFile = null 24 | 25 | loadFromFile: (directory) -> 26 | @lastFile = @getFullFilepath(directory) if directory? or not @lastFile? 27 | 28 | return unless @lastFile 29 | 30 | @parser = gitbookparse.getForFile(@lastFile) 31 | 32 | contents = fs.readFileSync(@lastFile, 'utf-8') 33 | @parseFileToTree(contents) 34 | 35 | reload: (clearFile) -> 36 | directory = if clearFile then @directory else null 37 | @loadFromFile(directory) 38 | 39 | getFullFilepath: (directory) -> 40 | jsonContents = @findAndParseBookJson(directory) 41 | 42 | if jsonContents and jsonContents.structure and jsonContents.structure.summary 43 | summaryName = jsonContents.structure.summary 44 | summaryPath = path.join(directory, summaryName) 45 | return summaryPath if fs.existsSync(summaryPath) 46 | 47 | return PathHelper.findExistingSummaryPath(directory) or false 48 | 49 | findAndParseBookJson: (directory) -> 50 | bookPath = path.join(directory, 'book.json') 51 | 52 | return false unless fs.existsSync(bookPath) 53 | 54 | contents = fs.readFileSync(bookPath, 'utf-8') 55 | try 56 | parsedContents = JSON.parse(contents) 57 | return parsedContents 58 | catch error 59 | false 60 | 61 | 62 | addSection: (name, path, parent, index) -> 63 | toWrite = {name: name, file: path, indent: 0} 64 | toWriteIndex = if index? then index else @tree.length 65 | 66 | for ele, idx in @tree 67 | if parent? and ele.file == parent 68 | toWriteIndex = idx + 1 69 | toWrite.indent = ele.indent + 2 70 | 71 | # Check for children of new parent if index was passed 72 | if toWriteIndex > 0 and index 73 | prevEl = @tree[toWriteIndex - 1] 74 | if prevEl? and prevEl.indent 75 | toWrite.indent = prevEl.indent 76 | # Look for child elements until you don't find an indent 77 | while (nextEl = @tree[toWriteIndex])? 78 | if nextEl.indent <= toWrite.indent 79 | break 80 | toWriteIndex++ 81 | 82 | @tree.splice toWriteIndex, 0, toWrite 83 | @tree 84 | 85 | 86 | onFileParsed: (callback) -> 87 | @emitter.on 'tree-parsing-complete', callback 88 | 89 | deleteSection: (filename) -> 90 | for ele, idx in @tree 91 | # Occasionally the tree gets borked? 92 | if ele? and ele.file == filename 93 | @tree.splice idx, 1 94 | @tree 95 | 96 | parseFileToTree: (contents) -> 97 | @parser.summary(contents).then (summary) => 98 | @tree = [] 99 | summary.chapters.forEach (chapter) => 100 | @addToTree(chapter, 0) 101 | console.log "Emitting tree parse" 102 | @emitter.emit 'tree-parsing-complete' 103 | 104 | addToTree: (chapter, indent) -> 105 | treeObj = indent: indent, name: chapter.title, file: chapter.path 106 | 107 | @tree.push(treeObj) 108 | 109 | return if not chapter.articles 110 | 111 | chapter.articles.forEach (article) => 112 | @addToTree(article, indent + 2) 113 | 114 | getSummaryWriter: (file) -> 115 | file = @lastFile if not file? 116 | 117 | extension = path.extname(file) 118 | switch extension 119 | when ".md", ".markdown", ".mdown" 120 | return require './markdown-summary' 121 | when ".asdoc", ".asciidoc", ".adoc" 122 | return require './asdoc-summary' 123 | when ".rst" 124 | return require './retext-summary' 125 | 126 | generateFileFromTree: (file) -> 127 | file = @lastFile if not file? 128 | 129 | writer = @getSummaryWriter(file) 130 | return if not writer? 131 | 132 | linestr = writer.formatTree(@tree) 133 | fs.writeFileSync(file, linestr) 134 | 135 | organizeFilesFromTree: (rootPath, file) -> 136 | lastIndent = 0; 137 | directory = basePath = atom.project.getPaths()[0] 138 | 139 | for ele, idx in @tree 140 | ele.file = slug(ele.name, {replacement: "_", lower: true}) + '.md' 141 | # TODO: Not quite right, doesn't cover situation where it jumps back up an indent level 142 | if ele.indent > lastIndent 143 | parentEl = @ensureEleFolderFormat(directory, basePath, previousElement) 144 | @tree[idx - 1] = parentEl 145 | 146 | parentPath = path.dirname(parentEl.file) 147 | directory = path.join(basePath, parentPath) 148 | 149 | newPath = path.join(parentPath, path.basename(ele.file)) 150 | existingPath = path.join(directory, ele.file) 151 | ele.file = newPath 152 | 153 | # fs.moveSync(existingPath, path.join(directory, newPath)) if fs.statSync(existingPath).isFile() 154 | else if ele.indent > 0 155 | # reverse iterate over the tree until you find the common parent 156 | curpos = idx 157 | for i in [idx..0] 158 | continue unless @tree[i].indent < ele.indent 159 | 160 | parentPath = path.dirname(@tree[i].file) 161 | newPath = path.join(parentPath, path.basename(ele.file)) 162 | oldPath = ele.file 163 | ele.file = newPath 164 | 165 | try 166 | fs.moveSync(oldPath, newPath) if fs.statSync(oldPath).isFile() 167 | 168 | break 169 | else if ele.indent == 0 170 | oldPath = ele.file 171 | directory = basePath 172 | 173 | try 174 | fs.moveSync(oldPath, ele.file) if fs.statSync(oldPath).isFile() 175 | 176 | previousElement = ele 177 | lastIndent = ele.indent 178 | 179 | @generateFileFromTree() 180 | 181 | # gitbook init if available and configured to do so? TODO: Finish and test 182 | # if atom.config.get('atom-gitbook.runGitbookInitAutomatically') 183 | # require('child-process').exec('gitbook init') 184 | 185 | ensureEleFolderFormat: (rootPath, basePath, ele) -> 186 | summaryFileName = atom.config.get('atom-gitbook.chapterSummaryFileName') 187 | return ele if path.basename(ele.file) == summaryFileName and path.dirname(ele.file) == basePath 188 | 189 | folderSlug = slug(path.basename(ele.file, 'md'), {replacement: "_", lower: true}) 190 | folderPath = path.join(rootPath, folderSlug) 191 | 192 | try 193 | fs.statSync(folderPath).isDirectory() 194 | catch 195 | fs.mkdirSync(folderPath) 196 | 197 | filename = path.join(folderPath, summaryFileName) 198 | 199 | existingPath = path.join(rootPath, ele.file) 200 | try 201 | fs.moveSync(existingPath, filename) if fs.statSync(existingPath).isFile() 202 | 203 | ele.file = path.relative(basePath, filename) 204 | ele 205 | -------------------------------------------------------------------------------- /lib/main.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | path = require 'path' 3 | {exec} = require 'child_process' 4 | {$} = require 'atom-space-pen-views' 5 | fs = require 'fs-plus' 6 | {Emitter} = require 'atom' 7 | IncludeParser = require './helper/include-parser' 8 | PathHelper = require './helper/path-helper' 9 | 10 | if not atom.packages.isPackageDisabled 'markdown-preview' 11 | MarkdownPreviewView = require path.join(atom.packages.resolvePackagePath('markdown-preview'), 'lib', 'markdown-preview-view') 12 | 13 | module.exports = 14 | config: 15 | removeFilesOnMenuDelete: 16 | type: 'string' 17 | default: 'No' 18 | enum : ['No', 'Yes', 'Ask'] 19 | autoOrganizeSummaryFileOnToCChange: 20 | title: "Reorder Files on Table of Contents Change" 21 | description: "Automatically creates a folder/file structure based on the Table of Contents when it's changed via the Table of Contents" 22 | type: 'boolean' 23 | default: false 24 | reportFolder: 25 | title: "Folder in which the report is located" 26 | description: "This is used when autogenerating a directory structure." 27 | type: 'string' 28 | default: './' 29 | runGitbookInitAutomatically: 30 | title: "Run gitbook init automatically" 31 | description: "On certian ToC Changes, gitbook init can be run to fill out underlying files." 32 | type: 'boolean' 33 | default: false 34 | autoOpenMarkdownPreview: 35 | title: "Automatically open the Markdown Preview" 36 | description: "When opening a file from the table of contents, automatically toggle the preview pane." 37 | type: 'boolean' 38 | default: true 39 | chapterSummaryFileName: 40 | title: "Chapter summary file name" 41 | type: 'string' 42 | default: 'README.md' 43 | 44 | gitbookView: null 45 | 46 | activate: (@state) -> 47 | @emitter = new Emitter 48 | @subscriptions = new CompositeDisposable 49 | @createView() 50 | 51 | @state.attached ?= true if @shouldAutoOpen() 52 | @togglePanel() if @state.attached 53 | 54 | @subscriptions.add atom.commands.add 'atom-workspace', 'atom-gitbook:toggle': => @togglePanel() 55 | @subscriptions.add atom.commands.add 'atom-workspace', 'atom-gitbook:force-reload-toc': => @forceReloadToC() 56 | @subscriptions.add atom.commands.add 'atom-workspace', 'atom-gitbook:organize-summary': => @organizeSummary() 57 | @subscriptions.add atom.commands.add '.gitbook-navigation-pane', 'atom-gitbook:new-chapter': => @newChapter() 58 | @subscriptions.add atom.commands.add '.gitbook-navigation-pane .gitbook-page-item', 'atom-gitbook:delete-chapter': => @deleteChapter() 59 | @subscriptions.add atom.commands.add '.tree-view.full-menu', 'atom-gitbook:add-file-as-chapter': => @addFileAsChapter() 60 | @subscriptions.add atom.commands.add '.tree-view.full-menu', 'atom-gitbook:insert-file-reference': => @insertFileReference() 61 | 62 | @subscriptions.add atom.commands.add 'atom-workspace', 'atom-gitbook:gitbook-init' : => @runGitbookCommand('init') 63 | 64 | if not atom.packages.isPackageDisabled 'markdown-preview' 65 | @subscriptions.add atom.workspace.observeActivePaneItem (pane) => @observePane(pane) 66 | 67 | observePane: (pane) -> 68 | unless pane instanceof MarkdownPreviewView 69 | return 70 | 71 | pane.onDidChangeMarkdown => 72 | return unless pane[0] 73 | replacedText = IncludeParser.parseIncludesInText(pane[0].innerHTML, pane.getPath()) 74 | IncludeParser.rerenderMarkdown(replacedText, pane.getPath()).then (html) => 75 | pane[0].innerHTML = html 76 | 77 | serialize: -> 78 | @state 79 | 80 | createView: -> 81 | unless @gitbookView? 82 | GitbookView = require './atom-gitbook-view' 83 | @gitbookView = new GitbookView 84 | @gitbookView # Return 85 | 86 | deactivate: -> 87 | @subscriptions.dispose() 88 | @gitbookView.destroy() 89 | 90 | newChapter: -> 91 | ChapterView = require './chapter-view' 92 | cv = new ChapterView() 93 | cv.attach() 94 | cv.onFileCreated => 95 | @createView().refresh() 96 | 97 | addFileAsChapter: -> 98 | selectedFile = $('.tree-view .selected .name') 99 | 100 | return unless selectedFile[0]? 101 | 102 | file = selectedFile[0].dataset 103 | wsPath = atom.project.getPaths()[0] 104 | 105 | Parser = require './helper/summary-parser' 106 | parser = Parser.getInstance(wsPath) 107 | 108 | parser.addSection(file.name, path.relative(wsPath, file.path)) 109 | parser.generateFileFromTree() 110 | 111 | @createView().refresh() 112 | 113 | organizeSummary: -> 114 | @createView().organizeSummary() 115 | 116 | deleteChapter: -> 117 | @createView().deleteChapter() 118 | 119 | shouldAutoOpen: -> 120 | return false unless atom.project.getPaths()[0]? 121 | 122 | wsPath = atom.project.getPaths()[0] 123 | if PathHelper.findExistingSummaryPath(wsPath) 124 | return true 125 | 126 | false 127 | 128 | insertFileReference: -> 129 | selectedFile = $('.tree-view .selected .name') 130 | return unless selectedFile[0]? 131 | sf = selectedFile[0].dataset 132 | 133 | editor = atom.workspace.getActiveTextEditor() 134 | return unless editor? 135 | 136 | wsPath = atom.project.getPaths()[0] 137 | console.log editor.getPath() 138 | console.log sf.path 139 | file = path.relative(wsPath, sf.path).replace(/\\/g, '/') 140 | editor.insertText("{% include \"#{file}\" %}") 141 | 142 | runGitbookCommand: (command) -> 143 | command = 'gitbook ' + command 144 | exec(command, {cwd: atom.project.getPaths()[0]}, (err, stdout, stderr) => 145 | console.log(err) 146 | console.log(stdout) 147 | console.log(stderr) 148 | ); 149 | 150 | forceReloadToC: -> 151 | return unless @open 152 | @createView().refresh(true, true) 153 | 154 | togglePanel: -> 155 | if @open 156 | @createView().hide() 157 | @open = false 158 | else 159 | @createView().show() 160 | @open = true 161 | -------------------------------------------------------------------------------- /lib/nav-pane.coffee: -------------------------------------------------------------------------------- 1 | Parser = require './helper/summary-parser' 2 | AtomGitbook = require './atom-gitbook' 3 | fs = require 'fs-plus' 4 | path = require 'path' 5 | {$, View} = require 'atom-space-pen-views' 6 | 7 | module.exports = 8 | class NavigationPane extends View 9 | @content: -> 10 | @div class: 'gitbook-navigation-pane', => 11 | @div class: 'gitbook-navigation-pane-label', => 12 | @h2 "Table of Contents" 13 | @div class: 'gitbook-navigation-container tool-panel', tabindex: -1, outlet: 'tree' 14 | 15 | initialize: -> 16 | @elementCache = {} 17 | @AtomGitbook = new AtomGitbook 18 | @getParser() 19 | @initEvents() 20 | 21 | getParser: -> 22 | if not @parser? 23 | @parser = Parser.getInstance(atom.project.getPaths()[0]) 24 | @parser.onFileParsed (e) => 25 | @refreshTree() 26 | @parser.reload() 27 | @parser 28 | 29 | refresh: (reloadFile = false, clearFile = false) -> 30 | @getParser().reload(clearFile) if reloadFile 31 | @getParser().organizeFilesFromTree() if atom.config.get('atom-gitbook.autoOrganizeSummaryFileOnToCChange') 32 | 33 | initEvents: -> 34 | @on 'dblclick', '.gitbook-page-item', (e) => 35 | ## Open File in Editor window if exists. 36 | if e.currentTarget.dataset.filename? 37 | @AtomGitbook.openEditorFile(e.currentTarget.dataset.filename, e.currentTarget.innerHTML) 38 | @on 'click', '.gitbook-page-item', (e) => 39 | @deselectMenuItems() unless e.shiftKey or e.metaKey or e.ctrlKey 40 | @selectElement(e.target) 41 | @on 'mousedown', '.gitbook-page-item', (e) => 42 | # Select the element if we're right clicking. 43 | if e.button is 2 44 | @deselectMenuItems() 45 | @selectElement(e.target) 46 | @on 'dragstart', '.gitbook-page-item', (e) => 47 | @draggedElement = e.target; 48 | e.stopPropagation() 49 | 50 | @on 'dragenter', '.gitbook-page-item, .gitbook-seperator', (e) => 51 | e.stopPropagation() 52 | @clearHovers() 53 | 54 | e.target.classList.add('gitbook-hover-target') 55 | 56 | @on 'dragleave', '.gitbook-page-item, .gitbook-seperator', (e) => 57 | e.preventDefault() 58 | e.stopPropagation() 59 | 60 | @on 'dragover', '.gitbook-page-item, .gitbook-seperator', (e) => 61 | e.preventDefault() 62 | e.stopPropagation() 63 | 64 | @on 'drop', '.gitbook-page-item, .gitbook-seperator', (e) => 65 | if @draggedElement 66 | elFile = e.target.dataset.filename if e.target.dataset.filename? 67 | index = e.target.dataset.index if e.target.dataset.index? 68 | 69 | ds = @draggedElement.dataset 70 | 71 | @getParser().deleteSection(ds.filename) 72 | @getParser().addSection(@draggedElement.innerHTML, ds.filename, elFile, index) 73 | @getParser().generateFileFromTree() 74 | @refresh(true) 75 | 76 | @draggedElement = null 77 | 78 | clearHovers: -> 79 | hoverTargets = document.querySelectorAll('.gitbook-hover-target') 80 | for h in hoverTargets 81 | h.classList.remove('gitbook-hover-target') 82 | 83 | selectElement: (ele) -> 84 | ele.classList.add('chapter-selected') 85 | 86 | deselectMenuItems: -> 87 | # Technique borrowed from tree view 88 | elements = @root.querySelectorAll('.chapter-selected') 89 | 90 | for element in elements 91 | element.classList.remove('chapter-selected') 92 | 93 | removeSelectedEntries: (deleteUnderlyingFiles = false) -> 94 | elements = @root.querySelectorAll('.chapter-selected') 95 | 96 | return unless elements? 97 | 98 | for ele in elements 99 | @parser.deleteSection(ele.dataset.filename) 100 | if deleteUnderlyingFiles 101 | fullPath = path.join(atom.project.getPaths()[0], ele.dataset.filename); 102 | fs.unlinkSync(fullPath) 103 | 104 | @getParser().generateFileFromTree() 105 | @refresh(true) 106 | 107 | refreshTree: -> 108 | @tree[0].removeChild(@tree[0].firstChild) while @tree[0].firstChild 109 | @elementCache = [] 110 | 111 | @root = document.createElement('ul') 112 | @root.classList.add('full-menu'); 113 | @root.classList.add('list-tree'); 114 | @root.classList.add('has-collapsable-children'); 115 | @elementCache[0] = [@root] 116 | 117 | for item, index in @parser.tree 118 | @genDepthElement(item, index) 119 | 120 | @tree.append(@root) 121 | 122 | genDepthElement: (treeEl, index) -> 123 | treeEl.indent = 0 unless treeEl.indent 124 | 125 | @elementCache[treeEl.indent] = [] unless @elementCache[treeEl.indent]? 126 | parentEl = @root 127 | 128 | if treeEl.indent > 0 129 | until parentIndent? and @elementCache[parentIndent]? 130 | if not parentIndent? 131 | parentIndent = treeEl.indent - 2; 132 | else 133 | parentIndent -= 2 134 | 135 | rootEl = @elementCache[parentIndent][@elementCache[parentIndent].length - 1] 136 | 137 | parentEl = document.createElement('ul') 138 | rootEl.appendChild(parentEl) 139 | 140 | @elementCache[treeEl.indent].push(parentEl) 141 | 142 | childEl = document.createElement('li') 143 | childEl.classList.add('gitbook-page-item') 144 | childEl.classList.add('icon-file-text') 145 | if treeEl.file 146 | childEl.dataset.filename = treeEl.file 147 | childEl.setAttribute('draggable', true) 148 | 149 | childEl.innerHTML = treeEl.name 150 | 151 | parentEl.appendChild(childEl) 152 | 153 | belowEl = document.createElement('div') 154 | belowEl.classList.add('gitbook-seperator') 155 | belowEl.dataset.index = index + 1 156 | 157 | parentEl.appendChild(belowEl) 158 | -------------------------------------------------------------------------------- /menus/atom-gitbook.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/hacking-atom-package-word-count#menus for more details 2 | 'context-menu': 3 | '.gitbook-navigation-pane': [ 4 | { 5 | 'label': 'New Chapter' 6 | 'command': 'atom-gitbook:new-chapter' 7 | } 8 | ], 9 | '.gitbook-navigation-pane .gitbook-page-item': [ 10 | { 11 | 'label' : 'Delete Chapter', 12 | 'command' : 'atom-gitbook:delete-chapter' 13 | } 14 | ], 15 | '.tree-view.full-menu' : [ 16 | { 17 | 'label' : 'Add As Chapter', 18 | 'command' : 'atom-gitbook:add-file-as-chapter' 19 | }, 20 | { 21 | 'label' : 'Insert as Gitbook Include', 22 | 'command' : 'atom-gitbook:insert-file-reference' 23 | } 24 | ] 25 | 'menu': [ 26 | { 27 | 'label': 'Packages' 28 | 'submenu': [ 29 | 'label': 'atom-gitbook' 30 | 'submenu': [ 31 | { 32 | 'label': 'Toggle' 33 | 'command': 'atom-gitbook:toggle' 34 | }, 35 | { 36 | 'label': 'Force Reload ToC' 37 | 'command': 'atom-gitbook:force-reload-toc' 38 | } 39 | ] 40 | ] 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-gitbook", 3 | "main": "./lib/main", 4 | "version": "0.1.4", 5 | "description": "Provides ToC and Markdown functionality for Gitbook", 6 | "keywords": [], 7 | "activationCommands": {}, 8 | "repository": "https://github.com/atom/atom-gitbook", 9 | "license": "MIT", 10 | "engines": { 11 | "atom": ">=1.0.0 <2.0.0" 12 | }, 13 | "dependencies": { 14 | "atom-space-pen-views": "2.1.0", 15 | "fs-plus": "2.8.1", 16 | "gitbook-parsers": "^0.8.9", 17 | "lodash": "^4.11.1", 18 | "slug": "0.9.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spec/atom-gitbook-spec.coffee: -------------------------------------------------------------------------------- 1 | AtomGitbook = require '../lib/atom-gitbook' 2 | 3 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 4 | # 5 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 6 | # or `fdescribe`). Remove the `f` to unfocus the block. 7 | 8 | describe "AtomGitbook", -> 9 | [workspaceElement, activationPromise, findNavPanel] = [] 10 | 11 | beforeEach -> 12 | workspaceElement = atom.views.getView(atom.workspace) 13 | activationPromise = atom.packages.activatePackage('atom-gitbook') 14 | 15 | findNavPanel = -> 16 | panels = atom.workspace.getLeftPanels() 17 | navPanel = null 18 | panels.forEach (panel) -> 19 | ## Feels like a hack.... 20 | navPanel = panel.getItem() if panel.item.constructor.name == 'NavigationPane' 21 | navPanel 22 | 23 | describe "when the atom-gitbook:toggle event is triggered", -> 24 | it "hides and shows the modal panel", -> 25 | expect(workspaceElement.querySelector('.gitbook-navigation-pane')).not.toExist() 26 | 27 | waitsForPromise -> 28 | activationPromise 29 | 30 | runs -> 31 | jasmine.attachToDOM(workspaceElement) 32 | atom.commands.dispatch workspaceElement, 'atom-gitbook:toggle' 33 | panel = findNavPanel() 34 | 35 | expect(panel).toExist() 36 | 37 | expect(panel.isVisible()).toBe true 38 | atom.commands.dispatch workspaceElement, 'atom-gitbook:toggle' 39 | expect(panel.isVisible()).toBe false 40 | -------------------------------------------------------------------------------- /spec/atom-gitbook-summary-parser-spec.coffee: -------------------------------------------------------------------------------- 1 | SummaryParser = require '../lib/helper/summary-parser' 2 | 3 | describe 'SummaryParser', -> 4 | 5 | [parserInstance] = [] 6 | 7 | beforeEach -> 8 | parserInstance = new SummaryParser('test') 9 | 10 | describe "Converting markdown lists to a tree.", -> 11 | it "should handle a single item", -> 12 | item = "* [Valid Markdown](readme.md)" 13 | 14 | parserInstance.parseFileToTree(item) 15 | tree = parserInstance.tree 16 | 17 | expect(tree.length).toBe 1 18 | expect(tree[0].indent).toBe 0 19 | expect(tree[0].name).toBe "Valid Markdown" 20 | expect(tree[0].file).toBe "readme.md" 21 | 22 | it "should handle multiple items on the same indent level", -> 23 | items = """ 24 | * [Valid Markdown](readme.md) 25 | * [More Valid Markdown](otherfile.md) 26 | """ 27 | parserInstance.parseFileToTree(items) 28 | tree = parserInstance.tree 29 | 30 | expect(tree.length).toBe 2 31 | expect(tree[1].indent).toBe 0 32 | expect(tree[1].name).toBe "More Valid Markdown" 33 | expect(tree[1].file).toBe "otherfile.md" 34 | 35 | it "shoud handle multiple items of different indentation levels", -> 36 | items = """ 37 | * [Valid Markdown](readme.md) 38 | * [Indented Item](indent.md) 39 | * [More Valid Markdown](otherfile.md) 40 | """ 41 | parserInstance.parseFileToTree(items) 42 | tree = parserInstance.tree 43 | 44 | expect(tree.length).toBe 3 45 | expect(tree[1].indent).toBe 2 46 | expect(tree[0].indent).toBe 0 47 | expect(tree[2].indent).toBe 0 48 | expect(tree[1].name).toBe "Indented Item" 49 | 50 | it "should handle numerically indexed summary files.", -> 51 | items = """ 52 | 1. [Valid Markdown](readme.md) 53 | 2. [Second Item](indent.md) 54 | 1. [More Valid Markdown](otherfile.md) 55 | """ 56 | parserInstance.parseFileToTree(items) 57 | tree = parserInstance.tree 58 | 59 | expect(tree.length).toBe 3 60 | expect(tree[1].name).toBe "Second Item" 61 | -------------------------------------------------------------------------------- /spec/atom-gitbook-view-spec.coffee: -------------------------------------------------------------------------------- 1 | AtomGitbookView = require '../lib/atom-gitbook-view' 2 | 3 | describe "AtomGitbookView", -> 4 | it "has one valid test", -> 5 | 6 | -------------------------------------------------------------------------------- /styles/atom-gitbook.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | .atom-gitbook { 4 | } 5 | 6 | .gitbook-navigation-pane { 7 | height: 100%; 8 | padding-left: 5px; 9 | border-right: 1px solid #666; 10 | border-left: 1px solid #666; 11 | padding-right: 20px; 12 | overflow: scroll; 13 | } 14 | 15 | .gitbook-navigation-pane h2 { 16 | padding-top: 5px; 17 | margin-top: 0; 18 | } 19 | 20 | .gitbook-navigation-container > ul ul { 21 | list-style-type: none; 22 | padding-left: 10px; 23 | } 24 | 25 | .gitbook-navigation-container ul li.chapter-selected { 26 | font-weight: bold; 27 | color: #fff; 28 | } 29 | 30 | .gitbook-page-item { 31 | -webkit-user-drag: element; 32 | } 33 | 34 | .gitbook-hover-target { 35 | background-color: #909; 36 | } 37 | 38 | .gitbook-seperator { 39 | height: 3px; 40 | -webkit-user-drag: element; 41 | } 42 | --------------------------------------------------------------------------------