├── .gitignore ├── BUILD.md ├── LICENSE ├── README.md ├── Slash.ico ├── Slash.png ├── browser ├── bower.json └── src │ ├── coffee │ ├── common.coffee │ ├── config.coffee │ ├── docsets.coffee │ └── search.coffee │ ├── jade │ ├── config.jade │ ├── docsets.jade │ └── index.jade │ └── less │ ├── components.less │ ├── config.less │ ├── importer.less │ └── main.less ├── coffee ├── args.coffee ├── bookmark.coffee ├── config.coffee ├── docset │ ├── db.coffee │ ├── index.coffee │ └── plist-reader.coffee ├── index.coffee └── server │ ├── api.coffee │ └── doc-server.coffee ├── gulpfile.coffee ├── makefile ├── package.json └── test ├── docset.coffee └── servers.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /Docsets 3 | /browser/bower_components 4 | /browser/assests 5 | /browser/*.html 6 | /.idea 7 | /browser/assets 8 | /index.js 9 | /js 10 | /local 11 | /profile 12 | /main 13 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Slash 构建方法 2 | 3 | Sorry for the inconvenience, but this file has only Chinese version now. 4 | 5 | If you want to translate it, please add BUILD_en.md to the root folder and send a PR. 6 | 7 | Slash 采用 electron 构建,理应全程无痛,但由于使用 node-sqlite3 的原因,导致需要编译一个模块。 8 | 9 | ## 获取 Slash 和 Electron 10 | 11 | [Slash 源码](https://github.com/oott123/Slash/archive/master.zip) | [Electron 下载](https://github.com/atom/electron/releases) 12 | 13 | 这里用 electron-v0.30.3-win32-ia32.zip 演示。 14 | 15 | ## 安装依赖、编译 html/css/js 16 | 17 | 打开命令行,进入 Slash 源码目录。 18 | 这里假设你已经安装好了 node 和 npm。 19 | 20 | ``` 21 | npm i 22 | npm i -g bower gulp 23 | .\node_modules\.bin\gulp 24 | cd browser 25 | ..\node_modules\.bin\bower install 26 | ``` 27 | 28 | ## 安装 sqlite3 for electron 29 | 30 | ``` 31 | cd node_modules\sqlite3 32 | node-gyp rebuild --target=0.30.3 --arch=ia32 --dist-url=https://atom.io/download/atom-shell --module_name=node_sqlite3 --module_path=..\lib\binding\node-v44-win32-ia32 33 | ``` 34 | 35 | ## 以 debug 模式启动 Slash 36 | 37 | ``` 38 | unzip electron-v0.30.3-win32-ia32.zip 39 | cd electron-v0.30.3-win32-ia32 40 | electron.exe ..\Slash-master -d 41 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 oott123 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | 版权所有 © 2014 oott123 22 | 23 | 这里以此方式许可任何人可以免费得到该软件拷贝以及附属文档文件 (“软件”), 24 | 并可任意处理该软件,包括无限制的使用、复制、修改、合并、出版、分发、转让 25 | 、授权和/或出售软件的权利,并允许购买该软件的人这样做,只要遵从以下条件: 26 | 27 | 以上的版权说明和许可说明应被包含在该软件任何部分的所有拷贝中。 28 | 29 | 该软件是以现状(“as is”)提供,没有任何保证、表示或暗示,包括但不限于适 30 | 销性、适用性和不侵权。在任何情况下作者不对任何声明、损坏或其他责任负责, 31 | 无论是发生在合同行为、侵权或其它任何来自软件的、与软件相关或无关的以及软 32 | 件的使用。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slash 2 | 3 | Slash is a [electron][electron] based, open source offline documents reader. 4 | We are devote to change the way of programmers read the documents. 5 | 6 | ## Help Us 7 | 8 | We Need : 9 | 10 | * Contributes on Slash of atom-shell/nodejs/javascript/css/Vue.js 11 | * A icon of Slash the application 12 | * A group of icons for [entry types][dash-entry-types] 13 | 14 | ## Features 15 | 16 | 1. Support Dash .docset 17 | 2. Provide integration APIs via local HTTP 18 | 19 | ## RoadMap 20 | 21 | 1. Docsets manager 22 | 2. In page index 23 | 3. Read The Docs support 24 | 4. Markdown files support 25 | 26 | # Slash 27 | 28 | Slash 是一个基于 [electron][electron] 构建的,开源的离线文档阅读器,致力于改变程序员阅读文档的方式。 29 | 30 | ## 需要你的协助! 31 | 32 | 目前项目还才刚刚开始,需要各位能人协助!我们需要: 33 | 34 | * atom-shell/nodejs/javascript/css/Vue.js 达人为 Slash 贡献代码 35 | * 一枚应用图标 36 | * 一组 Dash 的[类别图标][dash-entry-types] 37 | 38 | ## 拟支持特性 39 | 40 | 1. Docsets 管理器 41 | 2. 页面内目录 42 | 3. Read The Docs 支持 43 | 4. Markdown 文件支持 44 | 45 | ## 特性列表 46 | 47 | 1. 支持 Dash 的 .docset 文档 48 | 2. 提供监听在本地的 HTTP API 来操作 Slash 窗口实现可拓展的集成 49 | 50 | [electron]: https://github.com/atom/electron 51 | [dash-entry-types]: http://kapeli.com/docsets#supportedentrytypes -------------------------------------------------------------------------------- /Slash.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oott123/Slash/500d788b56c1f7f879af265830387bce06436f54/Slash.ico -------------------------------------------------------------------------------- /Slash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oott123/Slash/500d788b56c1f7f879af265830387bce06436f54/Slash.png -------------------------------------------------------------------------------- /browser/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slash-browser", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/oott123/Slash", 5 | "authors": [ 6 | "oott123 " 7 | ], 8 | "description": "Slash browser dependencies", 9 | "license": "MIT", 10 | "private": true, 11 | "dependencies": { 12 | "font-awesome": "~4.2.0", 13 | "vue": "~0.11.5", 14 | "jquery": "~2.1.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /browser/src/coffee/common.coffee: -------------------------------------------------------------------------------- 1 | window.S = window.S or {} 2 | window.ipc = require 'ipc' 3 | window.Vue = window.Vue or require './bower_components/vue/dist/vue.js' 4 | window.$ = require './bower_components/jquery/dist/jquery.js' 5 | remote = require 'remote' 6 | S.ds = remote.require './docset' 7 | S.docPort = remote.require('./server/doc-server').docPort 8 | S.cfg = remote.require('./config').config 9 | S.bookmarks = remote.require('./bookmark').bookmarks 10 | S.profileDir = remote.require('./args').profiledir 11 | S.ds.getDocsets().then (docsets)-> 12 | S.docsets = docsets 13 | S.args = remote.require('./args') -------------------------------------------------------------------------------- /browser/src/coffee/config.coffee: -------------------------------------------------------------------------------- 1 | remote = require 'remote' 2 | cfg = remote.require './config' 3 | config = cfg.config 4 | window.Vue = window.Vue or require './bower_components/vue/dist/vue.js' 5 | window.vm = new Vue 6 | el: 'body' 7 | data: 8 | configItems: [ 9 | {type: 'separator', title: 'Application Settings'} 10 | {type: 'int', key: 'maxItem', title: 'Max search result will display', value: 100} 11 | {type: 'int', key: 'searchDelay', title: 'Search delay when entering keyword', value: 300} 12 | {type: 'separator', title: 'Shortcut Settings (will affect after restart Slash)'} 13 | {type: 'text', key: 'shortCut', title: 'Global shortcut', value: 'Ctrl+A'} 14 | ] 15 | configNotChanged: true 16 | methods: 17 | setValue: (data) -> 18 | i = 0 19 | while i < @$data.configItems.length 20 | datum = @$data.configItems[i] 21 | if datum.key 22 | if data[datum.key] 23 | datum.value = data[datum.key] 24 | i++ 25 | vm.$watch 'configItems', -> 26 | @$data.configNotChanged = false 27 | closeWindow: -> 28 | window.close() 29 | saveConfig: -> 30 | data = {} 31 | i = 0 32 | while i < @$data.configItems.length 33 | datum = @$data.configItems[i] 34 | if datum.type is 'int' 35 | data[datum.key] = parseInt(datum.value) 36 | else if datum.type is 'number' 37 | data[datum.key] = parseNum(datum.value) 38 | else 39 | data[datum.key] = datum.value 40 | i++ 41 | cfg.config = data 42 | cfg.save() 43 | @$data.configNotChanged = true 44 | saveAndClose: -> 45 | @saveConfig() 46 | @closeWindow() 47 | Vue.nextTick -> 48 | vm.setValue config 49 | window.onbeforeunload = -> 50 | if !vm.$data.configNotChanged 51 | confirm "Close without saving changes?" -------------------------------------------------------------------------------- /browser/src/coffee/docsets.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | profileFile = path.join S.profileDir, 'docsets.json' 4 | 5 | try 6 | exports.profile = JSON.parse(fs.readFileSync profileFile) 7 | catch 8 | exports.profile = {} 9 | for i in S.docsets 10 | name = i.name 11 | exports.profile[name] = 12 | shortcut: i.name.replace('.docset', '').toLowerCase() 13 | enable: true 14 | exports.save = (data)-> 15 | fs.writeFileSync profileFile, JSON.stringify(data) 16 | exports.getDocsetByShortcut = (shortcut)-> 17 | shortcut = shortcut.toLowerCase() 18 | for i of exports.profile 19 | return i.name if i.shortcut is shortcut 20 | false 21 | exports.getDisabledDocsets = -> 22 | result = [] 23 | for i of exports.profile 24 | result.push i.name unless i.enabled 25 | result -------------------------------------------------------------------------------- /browser/src/coffee/search.coffee: -------------------------------------------------------------------------------- 1 | remote = require 'remote' 2 | _ = require 'lodash' 3 | 4 | docSetDir = remote.require('./args').docsetdir.replace(/\\/g, '/') 5 | 6 | # register global shortcut 7 | if S.cfg.shortCut 8 | ret = remote.require('global-shortcut').register S.cfg.shortCut, -> 9 | ipc.send 'showMainWindow' 10 | $('input#search').focus() 11 | window.onbeforeunload = -> 12 | remote.require('global-shortcut').unregister S.cfg.shortCut 13 | unless ret 14 | alert "Failed to register global shortcut #{S.cfg.shortCut}.\nCheck if it was already in use." 15 | 16 | Vue.component 'bookmark', 17 | template: '#bookmark-template' 18 | data: -> 19 | title: 'Dummy Bookmark' 20 | url: 'https://github.com/oott123/Slash' 21 | subItems: [] 22 | status: 23 | isEditing: false 24 | isOpen: false 25 | computed: 26 | isFolder: -> 27 | @subItems?.length 28 | isTop: -> 29 | @$parent.bookmarks 30 | methods: 31 | toggle: -> 32 | return if @isTop 33 | if @isFolder 34 | @status.isOpen = !@status.isOpen 35 | else 36 | url = @url.replace 'slash://', "http://localhost:#{S.docPort}/" 37 | S.vm.webContentSrc = url 38 | $('.button.bookmark').blur() 39 | edit: -> 40 | @status.isEditing = !@status.isEditing 41 | remove: -> 42 | if @$parent.subItems?[@$index] 43 | @$parent.subItems.splice @$index, 1 44 | else if @$parent.bookmarks?[@$index] 45 | @$parent.bookmarks.splice @$index, 1 46 | add: -> 47 | title = S.vm.title 48 | url = S.vm.webContentSrc.replace "http://localhost:#{S.docPort}/", 'slash://' 49 | data = 50 | title: title 51 | url: url 52 | status: 53 | isEditing: true 54 | @subItems = [] unless @subItems 55 | @subItems.push data 56 | @status.isOpen = true 57 | S.vm = new Vue 58 | el: 'html' 59 | data: 60 | results: [ 61 | {name: 'Hello Slash!'} 62 | ] 63 | webContentSrc: '' 64 | title: 'Welcome' 65 | docset: '' 66 | keyword: '' 67 | buttons: 68 | forward: false 69 | backward: false 70 | bookmark: true 71 | options: true 72 | bookmarks: _.clone(S.bookmarks, true) 73 | args: 74 | debug: S.args.debug 75 | methods: 76 | lazySearch: -> 77 | # check if ":" 78 | if matches = @keyword.match /^(.*):(.*)$/ 79 | $('#search').val(matches[2]) # changing @keyword won't works 80 | @docset = matches[1] 81 | # check if ";" 82 | if @keyword.match /;/ 83 | $('#search').val('') 84 | _.debounce(this.search, S.cfg.searchDelay).apply(this, arguments) 85 | search: -> 86 | keyword = @keyword 87 | if keyword.match /^https?:\/\// 88 | @webContentSrc = keyword 89 | return 90 | handle = new S.ds keyword, @docset 91 | loadedItems = {} 92 | processResult = (result)-> 93 | lt = loadedItems[result.docset.name] = loadedItems[result.docset.name] or {} 94 | for i in result.result 95 | return unless result.keyword is S.vm.$data.keyword 96 | return if S.vm.$data.results.length > S.cfg.maxItem 97 | continue if lt[i.id] 98 | lt[i.id] = true 99 | i.docset = 100 | name: result.docset.name 101 | S.vm.$data.results.push _.clone(i, true) 102 | handle.on 'finish', (allData)-> 103 | return if allData.length < 1 104 | return unless allData[0].keyword is S.vm.$data.keyword 105 | S.vm.$data.results = [] 106 | for res in allData 107 | processResult res 108 | Vue.nextTick -> 109 | $('ul#doc-list li:first').click() 110 | handle.match() 111 | loadWeb: (e)-> 112 | item = e.targetVM.result 113 | @webContentSrc = "http://localhost:#{S.docPort}/" + 114 | encodeURIComponent(item.docset.name) + 115 | "/Contents/Resources/Documents/#{item.path}" 116 | $('ul#doc-list li').removeClass 'active' 117 | $(e.target).addClass 'active' 118 | contentStartLoading: (e)-> 119 | @title = 'Loading ...' 120 | contentStopLoading: (e)-> 121 | # update title buttons 122 | @buttons.backward = e.target.canGoBack() 123 | @buttons.forward = e.target.canGoForward() 124 | contentPageTitleSet: (e)-> 125 | @title = e.title 126 | webNav: (i)-> 127 | $('#web-content')[0].goToOffset(i) 128 | message: (e)-> 129 | console.log e.message 130 | openInBrowser: -> 131 | require('shell').openExternal(@webContentSrc) 132 | $('.bars').blur() 133 | refreshBrowser: (e)-> 134 | $('#web-content')[0].reloadIgnoringCache() 135 | openConfigWindow: -> 136 | remote.require('./config').showConfigWindow() 137 | showDocsetStat: -> 138 | handle = new S.ds() 139 | handle.stat().then (data)-> 140 | str = "==== Docsets Index Item Count ====\n\n" 141 | for i in data 142 | str += "#{i.docset}: #{i.data}\n" 143 | alert(str) 144 | getBg: (name)-> 145 | "url('#{docSetDir}/" + 146 | encodeURIComponent(name) + 147 | "/icon.png')" 148 | reloadSlash: -> 149 | location.reload() 150 | toggleDevTools: -> 151 | S.args.mainWindow.toggleDevTools() 152 | S.vm.$watch 'docset + keyword', S.vm.lazySearch 153 | $('document').ready -> 154 | lastBackspace = 0 155 | $('input#search').keydown (e)-> 156 | next = false 157 | if e.which is 40 158 | # down 159 | next = $('ul#doc-list li.active').next() 160 | else if e.which is 38 161 | # up 162 | next = $('ul#doc-list li.active').prev() 163 | else if e.which is 8 164 | # backspace 165 | if $('#search').val() is '' and Date.now() - lastBackspace > 500 166 | S.vm.$data.docset = '' 167 | $('#docset').val('') 168 | lastBackspace = Date.now() 169 | if next.length 170 | next.click() 171 | st = next.parent().parent().scrollTop() 172 | next.parent().parent().scrollTop(st + next.offset().top - 300) 173 | e.stopPropagation() 174 | e.preventDefault() 175 | return false 176 | $('.menu.dropdown').click (e)-> 177 | $(this).parent().blur() 178 | $('.bookmark.button').focus -> 179 | $('.bookmarks').show() 180 | .blur -> 181 | return if isEditing(S.vm.bookmarks) 182 | $('.bookmarks').hide() 183 | # save bookmarks 184 | bmk = require('remote').require('./bookmark') 185 | bmk.save(JSON.stringify(S.vm.bookmarks, null, '\t')) 186 | isEditing = (items)-> 187 | for i in items 188 | return true if i.status?.isEditing 189 | if i.subItems 190 | return true if isEditing i.subItems 191 | return false -------------------------------------------------------------------------------- /browser/src/jade/config.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Options / Slash 5 | link(href="assets/css/importer.css", rel="stylesheet") 6 | meta(charset="utf-8") 7 | body.config 8 | .container 9 | .row(v-repeat="item: configItems") 10 | .separator.item(v-if="item.type=='separator'") 11 | | {{item.title}} 12 | .config.item.checkbox(v-if="item.type=='checkbox'") 13 | input(type="checkbox", v-model="item.value" id="{{item.key}}") 14 | label(for="{{item.key}}") {{item.title}} 15 | .config.item.select(v-if="item.type=='select'") 16 | label(for="{{item.key}}") {{item.title}} 17 | select(v-model="item.value" id="{{item.key}}") 18 | option(v-repeat="option: item.options", value="{{option.key}}") {{option.title}} 19 | .config.item.text(v-if="item.type == 'text' || item.type == 'int' || item.type == 'number'") 20 | label(for="{{item.key}}") {{item.title}} 21 | input(type="text", v-model="item.value" id="{{item.key}}") 22 | .ctrls 23 | button(v-on="click: saveAndClose") OK 24 | button(v-on="click: closeWindow") Cancel 25 | script(type="text/javascript", src="assets/js/common.js") 26 | script(type="text/javascript", src="assets/js/config.js") -------------------------------------------------------------------------------- /browser/src/jade/docsets.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Docsets / Slash 5 | meta(charset="utf8") 6 | link(rel="stylesheet" href="assets/css/importer.css") 7 | link(rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css") 8 | body.docsets 9 | script(src="assets/js/common.js") 10 | script(src="assets/js/docsets-run.js") 11 | -------------------------------------------------------------------------------- /browser/src/jade/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title {{title}} / Slash 5 | meta(charset="utf8") 6 | link(rel="stylesheet" href="assets/css/importer.css") 7 | link(rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css") 8 | body 9 | .main-wrapper 10 | .title-bar.sub-wrapper 11 | .search.left 12 | input#docset(type="text" v-model="docset") 13 | input#search(type="text" placeholder="Search" autofocus v-model="keyword") 14 | .nav.right 15 | .button-group.left 16 | .forward.button.left(v-class="disabled: !buttons.backward" 17 | v-on="click: webNav(-1)") 18 | i.fa.fa-angle-left 19 | .back.button.right(v-class="disabled: !buttons.forward" 20 | v-on="click: webNav(1)") 21 | i.fa.fa-angle-right 22 | #doc-title.title {{title}} 23 | .button-group.right 24 | .bookmark.button( 25 | v-class="disabled: !buttons.bookmark" 26 | tabindex=1) 27 | i.fa.fa-bookmark 28 | .bookmarks(v-component="bookmark" v-repeat="bookmarks") 29 | .bars.button( 30 | v-class="disabled: !buttons.options" 31 | tabindex=2) 32 | i.fa.fa-bars 33 | ul.menu.popup.dropdown 34 | li.buttons 35 | .button(v-on="click: openInBrowser") 36 | i.fa.fa-globe 37 | .button(v-on="click: refreshBrowser") 38 | i.fa.fa-refresh 39 | li.separate 40 | li(v-on="click: openConfigWindow") Settings ... 41 | li(v-on="click: showDocsetStat") Show Docsets ... 42 | li(v-on="click: webContentSrc = 'http://github.com/oott123/slash'") About ... 43 | .wrench.button(v-if="args.debug" tabindex=3) 44 | i.fa.fa-wrench 45 | ul.menu.popup.dropdown 46 | li(v-on="click: reloadSlash") Reload Slash 47 | li(v-on="click: toggleDevTools") Toggle Dev Tools 48 | .body.sub-wrapper 49 | .index.left 50 | ul#doc-list 51 | li(v-on="click: loadWeb" v-repeat="result: results" 52 | v-style="background-image: getBg(result.docset.name)") 53 | | {{result.name}} 54 | .content.right 55 | webview#web-content( 56 | v-attr="src: webContentSrc" 57 | v-on="did-stop-loading: contentStopLoading, " + 58 | "did-start-loading: contentStartLoading, " + 59 | "page-title-set: contentPageTitleSet, " + 60 | "console-message: message") 61 | .cover.config.hidden(v-class="hidden: !isConfigShow") 62 | iframe#config(src="{{configUrl}}" frameborder="0") 63 | script#bookmark-template(type="text/x-template") 64 | .title(v-class="hover: status.isEditing") 65 | .text.menu-item(v-on="click: toggle") {{title}} 66 | .ctrl.menu-item.add(v-on="click: add") 67 | i.fa.fa-plus 68 | .ctrl.menu-item.remove(v-on="click: remove" v-if="!isTop") 69 | i.fa.fa-times 70 | .ctrl.menu-item.edit(v-on="click: edit" v-if="!isTop" 71 | v-class="hover: status.isEditing") 72 | i.fa.fa-pencil 73 | form(v-if="status.isEditing") 74 | .label 75 | span Title 76 | input(v-model="title") 77 | .label(v-if="!isFolder") 78 | span Url 79 | input(v-model="url") 80 | ul.submenu(v-if="isTop || (isFolder && status.isOpen)") 81 | li(v-repeat="subItems" v-component="bookmark") 82 | script(src="assets/js/common.js") 83 | script(src="assets/js/search.js") 84 | -------------------------------------------------------------------------------- /browser/src/less/components.less: -------------------------------------------------------------------------------- 1 | .menu-style(){ 2 | -webkit-user-select: none; 3 | background-color: #FBFCFD; 4 | border: 1px solid #DCDDDE; 5 | box-shadow: #ddd 1px 1px 1px; 6 | list-style: none; 7 | text-align: left; 8 | font-size: 10pt; 9 | margin: 0 2px 2px; 10 | padding: 2px; 11 | } 12 | .menu{ 13 | .menu-style; 14 | &.popup{ 15 | display: none; 16 | position: fixed; 17 | right: 0; 18 | min-width: 150px; 19 | } 20 | li { 21 | .menu-item; 22 | padding: 0 20px; 23 | &.separate { 24 | height: 1px; 25 | background-color: #ccc; 26 | padding: 0; 27 | border: 0; 28 | margin: 2px 0; 29 | } 30 | &.buttons { 31 | font-size: 12pt; 32 | display: flex; 33 | flex-wrap: wrap; 34 | text-align: center; 35 | border: none; 36 | background-color: transparent; 37 | padding-left: 0; 38 | padding-right: 0; 39 | &:hover { 40 | background-color: transparent; 41 | } 42 | .button { 43 | .menu-item; 44 | flex: 1; 45 | display: inline-block; 46 | line-height: 32px; 47 | height: 32px; 48 | min-width: 32px; 49 | text-align: center; 50 | } 51 | } 52 | } 53 | } 54 | .menu-item { 55 | -webkit-user-select: none; 56 | cursor: default; 57 | border: 1px solid rgba(0,0,0,0); 58 | &:hover, &:focus, &.hover { 59 | background-color: #E5F3FB; 60 | border: 1px solid #70C0E7; 61 | } 62 | &.disabled, &.disabled:hover, &.disabled:focus { 63 | background-color: #ddd; 64 | color: #999; 65 | border: 1px solid #ddd; 66 | } 67 | } 68 | .cover { 69 | position: absolute; 70 | top: 0; 71 | left: 0; 72 | height: 100%; 73 | width: 100%; 74 | background-color: rgba(0,0,0,0.7); 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | } 79 | .hidden { 80 | display: none; 81 | } -------------------------------------------------------------------------------- /browser/src/less/config.less: -------------------------------------------------------------------------------- 1 | body.config { 2 | margin: 0; 3 | background-color: #fff; 4 | .item { 5 | &.separator { 6 | border-bottom: 1px dashed #ccc; 7 | padding-top: 5px; 8 | margin-bottom: 3px; 9 | padding-bottom: 3px; 10 | padding-left: 17px; 11 | font-size: 15px; 12 | color: #9A717B; 13 | font-weight: bold; 14 | line-height: 25px; 15 | } 16 | &.select label, &.text label { 17 | width: 50%; 18 | display: inline-block; 19 | text-align: right; 20 | margin-right: 21px; 21 | } 22 | } 23 | input{ 24 | width: 230px; 25 | height: 20px; 26 | } 27 | select{ 28 | width: 234px; 29 | height: 26px; 30 | } 31 | .item { 32 | font-size: 13px; 33 | line-height: 30px; 34 | .checkbox { 35 | padding-left: 20px; 36 | input { 37 | width: auto; 38 | } 39 | } 40 | } 41 | .container { 42 | padding-bottom: 60px; 43 | } 44 | .ctrls { 45 | height: 48px; 46 | width: 100%; 47 | box-shadow: 0px -3px 5px 0 rgba(50, 50, 50, 0.25); 48 | margin-top: 15px; 49 | background: #fff; 50 | position: fixed; 51 | bottom: 0; 52 | text-align: right; 53 | line-height: 48px; 54 | padding-right: 18px; 55 | right: 0; 56 | button { 57 | line-height: 24px; 58 | font-size: 12px; 59 | padding-left: 18px; 60 | padding-right: 18px; 61 | margin: 0 10px; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /browser/src/less/importer.less: -------------------------------------------------------------------------------- 1 | @import "components.less"; 2 | @import "main.less"; 3 | @import "config.less"; -------------------------------------------------------------------------------- /browser/src/less/main.less: -------------------------------------------------------------------------------- 1 | body, html { 2 | font-family:"Lantinghei SC","微软雅黑","Microsoft Yahei","文泉驿微米黑","WenQuanYi Micro Hei",sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | height: 100%; 6 | } 7 | 8 | .main-wrapper { 9 | height: 100%; 10 | width: 100%; 11 | overflow: hidden; 12 | } 13 | 14 | @title-height: 34px; 15 | 16 | .sub-wrapper { 17 | height: 100%; 18 | display: flex; 19 | overflow: hidden; 20 | > .left { 21 | width: 300px; 22 | } 23 | > .right { 24 | flex: 1; 25 | } 26 | } 27 | 28 | .title-bar { 29 | text-align: center; 30 | height: @title-height; 31 | line-height: 32px; 32 | background-color: #ccc; 33 | > .search input { 34 | font-family: "Source Code Pro", monospace; 35 | &#search { 36 | width: 60%; 37 | } 38 | &#docset { 39 | width: 20%; 40 | } 41 | } 42 | > .nav { 43 | display: flex; 44 | > .button-group { 45 | -webkit-user-select: none; 46 | > .button:focus { 47 | outline: none; 48 | > .dropdown { 49 | display: block; 50 | } 51 | } 52 | } 53 | > .title { 54 | flex: 1; 55 | } 56 | } 57 | } 58 | 59 | .button-group { 60 | > .button { 61 | display: inline-block; 62 | width: 32px; 63 | box-sizing: border-box; 64 | background: #fff; 65 | border: 1px solid #fff; 66 | cursor: default; 67 | transition: background, border 200ms; 68 | .menu-item; 69 | } 70 | } 71 | 72 | .sub-wrapper.body { 73 | box-sizing: border-box; 74 | padding-bottom: @title-height; 75 | > .index { 76 | overflow-x: hidden; 77 | overflow-y: auto; 78 | ul { 79 | list-style-type: none; 80 | padding: 0; 81 | margin: 0 0 0 0; 82 | height: 100%; 83 | li { 84 | background-repeat: no-repeat; 85 | background-position: 5px center; 86 | cursor: default; 87 | padding: 4px 10px 4px 24px; 88 | font-size: 10pt; 89 | line-height: 18px; 90 | word-break: break-all; 91 | word-wrap: break-word; 92 | overflow: hidden; 93 | height: 18px; 94 | border: 1px solid rgba(0,0,0,0); 95 | .menu-item; 96 | &.active { 97 | background-color: #D1E8FF; 98 | border: 1px solid #70C0E7; 99 | } 100 | &:hover + li.active { 101 | border-top-color: rgba(0,0,0,0); 102 | } 103 | &.active + li:hover { 104 | border-top-color: rgba(0,0,0,0); 105 | } 106 | } 107 | } 108 | } 109 | > .content { 110 | height: 100%; 111 | webview { 112 | height: 100%; 113 | width: 100%; 114 | } 115 | } 116 | } 117 | 118 | iframe#config { 119 | height: 80%; 120 | width: 80%; 121 | max-width: 700px; 122 | max-height: 300px; 123 | } 124 | 125 | .bookmarks { 126 | .menu-style; 127 | display: none; 128 | position: absolute; 129 | right: 0; 130 | min-width: 280px; 131 | .text, .ctrl { 132 | display: inline-block; 133 | } 134 | .text { 135 | padding-left: 10px; 136 | } 137 | .title:hover .ctrl, .title.hover .ctrl{ 138 | display: block; 139 | } 140 | .ctrl { 141 | display: none; 142 | text-align: center; 143 | font-size: 12pt; 144 | width: 32px; 145 | height: 32px; 146 | line-height: 32px; 147 | } 148 | .title { 149 | display: flex; 150 | .text { 151 | flex: 1; 152 | } 153 | } 154 | form { 155 | .label { 156 | span { 157 | display: inline-block; 158 | width: 50px; 159 | } 160 | } 161 | } 162 | ul { 163 | list-style: none; 164 | margin: 0; 165 | padding-left: 5px; 166 | } 167 | } -------------------------------------------------------------------------------- /coffee/args.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | fs = require 'fs' 3 | module.exports = args = 4 | profiledir: path.join process.cwd(), 'profile' 5 | shortArgs = { 6 | 'd': 'debug' 7 | } 8 | for i in process.argv 9 | continue unless i.indexOf('-') == 0 10 | matches = i.match(/^-([^-].*)$/) 11 | matchesLong = i.match(/^--([^=]*)=(.*)$/) 12 | matchesLongSwitch = i.match(/^--([^=]*)$/) 13 | if matches?.length 14 | tags = matches[1] 15 | for x in tags 16 | if shortArgs[x] 17 | long = shortArgs[x] 18 | args[long] = true 19 | else if matchesLong?.length 20 | args[matchesLong[1].toLowerCase()] = matchesLong[2] 21 | else if matchesLongSwitch?.length 22 | args[matchesLongSwitch[1].toLowerCase()] = true 23 | try 24 | fs.statSync args.profiledir 25 | catch 26 | fs.mkdirSync args.profiledir -------------------------------------------------------------------------------- /coffee/bookmark.coffee: -------------------------------------------------------------------------------- 1 | args = require './args' 2 | path = require 'path' 3 | fs = require 'fs' 4 | 5 | bmkFile = path.join args.profiledir, 'bookmarks.json' 6 | bookmarks = [] 7 | try 8 | bookmarks = JSON.parse(fs.readFileSync bmkFile) 9 | catch err 10 | console.error(err) 11 | bookmarks = [ 12 | { 13 | title: 'Bookmarks' 14 | subItems: [ 15 | { 16 | title: 'Slash Project' 17 | subItems: [ 18 | { 19 | title: 'GitHub' 20 | url: 'https://github.com/oott123/Slash' 21 | } 22 | { 23 | title: 'Issues' 24 | url: 'https://github.com/oott123/Slash/issues' 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | ] 31 | exports.bookmarks = bookmarks 32 | exports.save = (newBmks)-> 33 | exports.bookmarks = JSON.parse(newBmks) 34 | fs.writeFileSync bmkFile, newBmks -------------------------------------------------------------------------------- /coffee/config.coffee: -------------------------------------------------------------------------------- 1 | args = require './args' 2 | fs = require 'fs' 3 | path = require 'path' 4 | _ = require 'lodash' 5 | browserWindow = require 'browser-window' 6 | defaultConfig = 7 | maxItem: 100 8 | searchDelay: 300 9 | shortCut: 'Alt+Enter' 10 | configFile = path.join(args.profiledir, 'config.json') 11 | exports.save = -> 12 | fs.writeFileSync configFile, JSON.stringify(exports.config, null, '\t') 13 | 14 | try 15 | data = fs.readFileSync configFile 16 | exports.config = _.extend(defaultConfig, JSON.parse(data)) 17 | catch e 18 | exports.config = defaultConfig 19 | exports.configWindow = null 20 | exports.showConfigWindow = -> 21 | exports.configWindow = new browserWindow 22 | width: 600 23 | height: 300 24 | "web-preferences": 25 | "direct-write": true 26 | "overlay-scrollbars": false 27 | icon: path.join(path.dirname(__dirname), 'Slash.png') 28 | exports.configWindow.loadUrl 'file://' + __dirname + '/../browser/config.html' 29 | exports.closeConfigWindow = -> 30 | exports.configWindow.close() -------------------------------------------------------------------------------- /coffee/docset/db.coffee: -------------------------------------------------------------------------------- 1 | Promise = require 'bluebird' 2 | knex = require 'knex' 3 | 4 | config = require('../config').config 5 | 6 | class module.exports 7 | constructor: (@fileName) -> 8 | @k = knex 9 | client: 'sqlite3' 10 | connection: 11 | filename: @fileName 12 | @k.schema.hasTable('searchIndex').then (isZDash)=> 13 | @isZDash = !isZDash 14 | s: -> 15 | @k('searchIndex') 16 | nameLike: (match, limit = config.maxItem / 2)-> 17 | match = match.replace /([%_])/g, '\\$1' 18 | .replace /\?/g, '_' 19 | .replace /\*/g, '%' 20 | if @isZDash 21 | @k.raw ' 22 | SELECT 23 | ztoken.z_pk AS id, 24 | ztoken.ztokenname AS name, 25 | ztokentype.ztypename AS type, 26 | printf("%s#%s", zfilepath.zpath, ztokenmetainformation.zanchor) AS path 27 | FROM ztoken 28 | JOIN ztokenmetainformation ON ztoken.zmetainformation = ztokenmetainformation.z_pk 29 | JOIN zfilepath ON ztokenmetainformation.zfile = zfilepath.z_pk 30 | JOIN ztokentype ON ztoken.ztokentype = ztokentype.z_pk 31 | WHERE name LIKE ? ESCAPE "\\" COLLATE NOCASE LIMIT ? 32 | ', [match, limit] 33 | else 34 | @s().whereRaw('name LIKE ? ESCAPE "\\" COLLATE NOCASE', match).limit(limit) 35 | getIndexCount: -> 36 | if @isZDash 37 | @k('ztoken').count('ztokenname') 38 | .then (rows) -> 39 | rows[0]['count("ztokenname")'] 40 | else 41 | @s().count('name') 42 | .then (rows)-> 43 | rows[0]['count("name")'] 44 | matchExactly: (key, limit = 1)-> 45 | @nameLike(key, limit) 46 | matchHead: (string)-> 47 | @nameLike("#{string}*") 48 | matchTail: (string)-> 49 | @nameLike("*#{string}") 50 | matchMiddle: (string)-> 51 | @nameLike("*#{string}*") 52 | matchDeep: (string)-> 53 | match = '' 54 | depth = 1 55 | for char, i in string 56 | match += '*' unless i % depth 57 | match += char 58 | @nameLike("*#{match}*") -------------------------------------------------------------------------------- /coffee/docset/index.coffee: -------------------------------------------------------------------------------- 1 | db = require './db' 2 | plistReader = require './plist-reader' 3 | fs = require 'fs' 4 | Promise = require 'bluebird' 5 | args = require '../args' 6 | path = require 'path' 7 | config = require('../config').config 8 | 9 | docsets = [] 10 | 11 | getDocsets = (forceUpdate = false)-> 12 | if !forceUpdate and docsets.length > 0 13 | Promise.resolve docsets 14 | else 15 | docsets = [] 16 | promises = [] 17 | files = fs.readdirSync args.docsetdir 18 | for i in files 19 | do (i)-> 20 | docsetDir = path.join args.docsetdir, i 21 | if fs.statSync(docsetDir).isDirectory() 22 | return unless i.match /\.docset$/ 23 | return unless fs.statSync(path.join(docsetDir, "Contents/Info.plist")).isFile() 24 | return unless fs.statSync(path.join(docsetDir, "Contents/Resources/docSet.dsidx")).isFile() 25 | promises.push( 26 | new plistReader(path.join(docsetDir, "Contents/Info.plist")).parse() 27 | .then (meta)-> 28 | docset = 29 | name: i 30 | docsetDir: docsetDir 31 | meta: meta 32 | db: new db(path.join(docsetDir, "Contents/Resources/docSet.dsidx")) 33 | docsets.push docset 34 | docset 35 | ) 36 | Promise.all(promises) 37 | class AbortedByUser 38 | class module.exports extends require('events').EventEmitter 39 | constructor: (@keyword, @docset)-> 40 | match: -> 41 | that = this 42 | getDocsets() 43 | .then (docsets)-> 44 | promises = [] 45 | limitDocset = isGlob = false 46 | keyword = that.keyword 47 | if that.docset 48 | limitDocset = that.docset 49 | .replace /([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1" 50 | .replace /\\\\?/g, '.' 51 | .replace /\\\\*/g, '.*' 52 | limitDocset = new RegExp limitDocset, 'i' 53 | console.log "Limited Docset: #{limitDocset}" 54 | if keyword.indexOf('?') >= 0 or keyword.indexOf('*') >= 0 55 | console.log 'isGlob' 56 | isGlob = true 57 | for method in ['matchExactly', 'matchHead', 'matchTail', 'matchMiddle', 'matchDeep'] 58 | for docset in docsets 59 | if limitDocset and !docset.meta.CFBundleName.match limitDocset 60 | console.log 'skipped: ' + docset.name 61 | continue 62 | do (method, docset)-> 63 | promises.push(new Promise (resolve)-> 64 | if isGlob 65 | return resolve docset.db[method](keyword, config.maxItem) 66 | resolve docset.db[method](keyword) 67 | .catch (e)-> 68 | console.log "Error when accessing docset #{docset.name}." 69 | console.log e 70 | [] 71 | .then (data)-> 72 | result: data 73 | keyword: that.keyword 74 | method: method 75 | docset: docset 76 | ) 77 | if isGlob 78 | break # let it run just matchExactly 79 | Promise.all promises 80 | .then (allData)-> 81 | that.emit 'finish', allData 82 | .catch (err)-> 83 | that.emit 'error', err 84 | .finally -> 85 | that.emit 'finally' 86 | stat: -> 87 | getDocsets() 88 | .then (docsets)-> 89 | promises = [] 90 | for docset in docsets 91 | do (docset)-> 92 | promises.push( 93 | docset.db.getIndexCount().catch (e)-> 94 | "Error: #{e.message}" 95 | .then (data)-> 96 | docset: docset.meta.CFBundleName 97 | data: data 98 | ) 99 | Promise.all promises 100 | module.exports.getDocsets = getDocsets -------------------------------------------------------------------------------- /coffee/docset/plist-reader.coffee: -------------------------------------------------------------------------------- 1 | Promise = require 'bluebird' 2 | plist = require 'plist' 3 | fs = require 'fs' 4 | class module.exports 5 | constructor: (@fileName, @plistData = null) -> 6 | parse: -> 7 | that = this 8 | new Promise (reslove, reject)-> 9 | if that.plistData 10 | return reslove that.plistData 11 | fs.readFile that.fileName, (err, res)-> 12 | reject err if err 13 | reslove res.toString() 14 | .then (plistData)-> 15 | that.plistData = plist.parse plistData 16 | that.plistData -------------------------------------------------------------------------------- /coffee/index.coffee: -------------------------------------------------------------------------------- 1 | app = require 'app' 2 | browserWindow = require 'browser-window' 3 | path = require 'path' 4 | fs = require 'fs' 5 | dialog = require 'dialog' 6 | ipc = require 'ipc' 7 | menu = require 'menu' 8 | tray = require 'tray' 9 | Promise = require 'bluebird' 10 | http = require 'http' 11 | 12 | args = require './args' 13 | defaultDocPort = args.docport or 33300 14 | defaultApiPort = args.apiport or 33400 15 | args.docsetdir = args.docsetdir or path.join(process.cwd(), "Docsets") 16 | 17 | mainWindow = null 18 | 19 | app.on 'ready', -> 20 | # check if Slash is running 21 | new Promise (resolve, reject)-> 22 | fs.readFile path.join(args.profiledir, 'api.port'), (err, data)-> 23 | return reject(err) if err 24 | port = parseInt data 25 | resolve port 26 | .catch -> 27 | # the api.port may not exist 28 | false 29 | .then (oldPort)-> 30 | new Promise (resolve, reject)-> 31 | resolve() unless oldPort 32 | options = 33 | hostname: 'localhost' 34 | port: oldPort 35 | path: '/focus' 36 | method: 'GET' 37 | req = http.request options, -> 38 | console.log 'Slash is already running !' 39 | reject(new Error('Slash is already running')) 40 | req.on 'error', (e)-> 41 | console.log e 42 | resolve() 43 | req.end() 44 | setTimeout -> 45 | resolve() 46 | , 1000 47 | .catch (e)-> 48 | # slash is running; focus it and quit self 49 | app.quit() 50 | throw e 51 | .then -> 52 | require('./server/doc-server').run(defaultDocPort, args.docsetdir) 53 | .then -> 54 | require('./server/api').run(defaultApiPort) 55 | .then (apiPort)-> 56 | fs.writeFileSync path.join(args.profiledir, 'api.port'), apiPort 57 | .then -> 58 | args.mainWindow = mainWindow = new browserWindow 59 | width: 1000 60 | height: 710 61 | "web-preferences": 62 | "direct-write": true 63 | "overlay-scrollbars": false 64 | icon: path.join(path.dirname(__dirname), 'Slash.png') 65 | mainWindow.loadUrl 'file://' + __dirname + '/../browser/index.html' 66 | app.on 'window-all-closed', -> 67 | fs.unlinkSync path.join(args.profiledir, 'api.port') 68 | app.quit() 69 | ipc.on 'showMainWindow', -> 70 | args.mainWindow.focus() 71 | args.mainWindow.focusOnWebView() -------------------------------------------------------------------------------- /coffee/server/api.coffee: -------------------------------------------------------------------------------- 1 | connect = require 'connect' 2 | http = require 'http' 3 | Promise = require 'bluebird' 4 | url = require 'url' 5 | 6 | args = require '../args' 7 | 8 | exports.apiPort = 0 9 | createServer = (app, port, resolve, reject)-> 10 | console.log "Try to create api server on port #{port}" 11 | http.createServer(app) 12 | .once('error', (e)-> 13 | return createServer app, port+1, resolve, reject if e.code is 'EADDRINUSE' 14 | reject e 15 | ).once('listening', -> 16 | exports.apiPort = port 17 | resolve(port) 18 | ).listen(port, 'localhost') 19 | focus = -> 20 | args.mainWindow.minimize() 21 | args.mainWindow.focus() 22 | args.mainWindow.focusOnWebView() 23 | exports.run = (port)-> 24 | new Promise (resolve, reject)-> 25 | return resolve exports.apiPort if exports.apiPort 26 | app = connect() 27 | app.use (req, res, next)-> 28 | req.urlParams = url.parse req.url, true, true 29 | res.json = (body, code = 200) -> 30 | res.writeHead code, {'Content-Type': 'application/json'} 31 | res.write JSON.stringify(body) 32 | res.end() 33 | res.reject = (reason, code = 400) -> 34 | res.json({error: reason}, code) 35 | next() 36 | app.use '/ping', (req, res)-> 37 | res.json true 38 | app.use '/focus', (req, res)-> 39 | focus() 40 | res.json true 41 | app.use '/window-search', (req, res)-> 42 | return res.reject 'Missing argument: keyword' unless req.urlParams.query.keyword 43 | args.mainWindow.webContents.executeJavaScript( 44 | "S.vm.$data.keyword = #{JSON.stringify(req.urlParams.query.keyword)}") 45 | if req.urlParams.query.docset 46 | args.mainWindow.webContents.executeJavaScript( 47 | "S.vm.$data.docset = #{JSON.stringify(req.urlParams.query.docset)}") 48 | focus() 49 | return res.json(true) 50 | app.use (req, res)-> 51 | res.reject 'Method Not Found' 52 | res.end() 53 | createServer app, port, resolve, reject -------------------------------------------------------------------------------- /coffee/server/doc-server.coffee: -------------------------------------------------------------------------------- 1 | connect = require 'connect' 2 | serveStatic = require 'serve-static' 3 | http = require 'http' 4 | Promise = require 'bluebird' 5 | exports.docPort = 0 6 | createServer = (app, port, resolve, reject)-> 7 | console.log "Try to create doc server on port #{port}" 8 | http.createServer(app) 9 | .once('error', (e)-> 10 | return createServer app, port+1, resolve, reject if e.code is 'EADDRINUSE' 11 | reject e 12 | ).once('listening', -> 13 | exports.docPort = port 14 | resolve(port) 15 | ).listen(port, 'localhost') 16 | exports.run = (port, root)-> 17 | new Promise (resolve, reject)-> 18 | return resolve exports.docPort if exports.docPort 19 | app = connect() 20 | app.use serveStatic(root) 21 | app.use (req, res)-> 22 | res.end() 23 | createServer app, port, resolve, reject 24 | 25 | -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | sourceDir = 'browser/src' 2 | destDir = 'browser/assets' 3 | viewsDir = 'browser' 4 | 5 | gulp = require 'gulp' 6 | clean = require 'gulp-clean' 7 | jade = require 'gulp-jade' 8 | less = require 'gulp-less' 9 | coffee = require 'gulp-coffee' 10 | 11 | gulp.task 'clean', -> 12 | gulp.src([destDir, "#{viewsDir}/*.html"]).pipe(clean()) 13 | 14 | gulp.task 'html', -> 15 | gulp.src "#{sourceDir}/jade/**/*.jade" 16 | .pipe jade() 17 | .pipe gulp.dest viewsDir 18 | 19 | gulp.task 'css', -> 20 | gulp.src "#{sourceDir}/less/importer.less" 21 | .pipe less() 22 | .pipe gulp.dest "#{destDir}/css" 23 | 24 | gulp.task 'js', -> 25 | gulp.src "#{sourceDir}/coffee/**/*.coffee" 26 | .pipe coffee() 27 | .pipe gulp.dest("#{destDir}/js") 28 | 29 | gulp.task 'static', -> 30 | gulp.src "#{sourceDir}/static/**" 31 | .pipe gulp.dest "#{destDir}" 32 | 33 | gulp.task 'main-js', -> 34 | gulp.src "coffee/**/*.coffee" 35 | .pipe coffee() 36 | .pipe gulp.dest "main/" 37 | 38 | gulp.task 'default', ['html', 'static', 'js', 'css', 'main-js'], -> 39 | # default task 40 | 41 | gulp.task 'watch', ['default'], -> 42 | gulp.watch "#{sourceDir}/coffee/**/*.coffee", ['js'] 43 | gulp.watch "#{sourceDir}/less/**/*.less", ['css'] 44 | gulp.watch "#{sourceDir}/jade/**/*.jade", ['html'] 45 | gulp.watch "#{sourceDir}/static/**/*", ['static'] 46 | gulp.watch "coffee/**/*.coffee", ['main-js'] 47 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | all: 2 | npm install 3 | cd browser 4 | bower install 5 | cd .. 6 | gulp 7 | watch: 8 | gulp watch 9 | test: 10 | mocha --compilers coffee:coffee-script/register -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Slash", 3 | "version": "0.1.0", 4 | "description": "A electron based document reader.", 5 | "main": "main/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "bluebird": "2.9.13", 13 | "connect": "3.3.4", 14 | "knex": "0.7.4", 15 | "lodash": "3.3.1", 16 | "plist": "1.1.0", 17 | "serve-static": "1.9.1", 18 | "simple-plist": "0.0.3", 19 | "sqlite3": "3.0.10" 20 | }, 21 | "devDependencies": { 22 | "bower": "1.4.1", 23 | "should": "5.0.1", 24 | "mocha": "2.1.0", 25 | "gulp-clean": "0.3.1", 26 | "gulp-coffee": "2.2.0", 27 | "gulp": "3.8.10", 28 | "gulp-jade": "0.10.0", 29 | "gulp-less": "2.0.1", 30 | "coffee-script": "1.8.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/docset.coffee: -------------------------------------------------------------------------------- 1 | should = require 'should' 2 | 3 | plistReader = require '../coffee/docset/plist-reader' 4 | db = require '../coffee/docset/db' 5 | ds = require '../coffee/docset/index' 6 | fs = require 'fs' 7 | Promise = require 'bluebird' 8 | 9 | # Get the docsets list 10 | docsets = [] 11 | files = fs.readdirSync 'Docsets' 12 | for i in files 13 | docsetDir = "Docsets/#{i}" 14 | docsets.push docsetDir if fs.statSync(docsetDir).isDirectory() 15 | 16 | describe 'Plist reader test', -> 17 | it 'should parse docsets correctly', (done)-> 18 | isDocset = (data)-> 19 | data.should.have.property 'CFBundleIdentifier' 20 | data.should.have.property 'CFBundleName' 21 | data.should.have.property 'DocSetPlatformFamily' 22 | data.should.have.property 'isDashDocset', true 23 | files = fs.readdirSync 'Docsets' 24 | promises = [] 25 | for docsetDir in docsets 26 | promises.push new plistReader("#{docsetDir}/Contents/Info.plist").parse() 27 | Promise.all(promises) 28 | .then (data)-> 29 | data.map isDocset 30 | null 31 | .then done 32 | .catch (e)-> 33 | throw e 34 | describe 'Search index test', -> 35 | it 'should get search index count correctly', (done)-> 36 | promises = [] 37 | for docsetDir in docsets 38 | d = new db "#{docsetDir}/Contents/Resources/docSet.dsidx" 39 | promises.push d.getIndexCount() 40 | Promise.all promises 41 | .then (data)-> 42 | data.map (count)-> 43 | count.should.above 0 44 | null 45 | .then done 46 | describe 'Docset match test', -> 47 | it 'should get some result', (done)-> 48 | handle = new ds 'f' 49 | hasResult= false 50 | handle.on 'result', (data)-> 51 | hasResult = true 52 | data.should.have.property 'keyword' 53 | data.should.have.property 'method' 54 | data.should.have.property 'docset' 55 | data.should.have.property('result').which.is.Array 56 | handle.on 'finish', -> 57 | hasResult.should.eql true 58 | done() 59 | handle.on 'error', (err)-> 60 | throw err 61 | handle.match() 62 | it 'should aborted correctly', (done)-> 63 | handle = new ds 'f' 64 | handle.on 'result', -> 65 | throw new Error "The match generated results after aborted it." 66 | handle.on 'finish', -> 67 | throw new Error "The match finished after aborted it." 68 | handle.on 'error', (err)-> 69 | throw err 70 | handle.on 'abort', -> 71 | done() 72 | handle.match() 73 | handle.abort() -------------------------------------------------------------------------------- /test/servers.coffee: -------------------------------------------------------------------------------- 1 | should = require 'should' 2 | Promise = require 'bluebird' 3 | path = require 'path' 4 | 5 | apiServer = require '../coffee/server/api' 6 | docServer = require '../coffee/server/doc-server' 7 | 8 | describe 'Servers running', -> 9 | it 'should start doc server correctly', (done)-> 10 | docServer.run 33300, path.join(__dirname, '../Docsets') 11 | .then (port)-> 12 | port.should.be.a.Number.which.is.above 33299 13 | done() 14 | it 'should start api server correctly', (done)-> 15 | apiServer.run 33400 16 | .then (port)-> 17 | port.should.be.a.Number.which.is.above 33399 18 | done() 19 | describe 'Servers running', -> 20 | @timeout 100000000 21 | it 'should start api server correctly', (done)-> 22 | #done() --------------------------------------------------------------------------------