├── Gemfile ├── lib ├── epic-editor-rails.rb └── epic-editor-rails │ ├── version.rb │ └── engine.rb ├── gemfiles ├── rails-3.2.gemfile ├── rails-4.0.gemfile └── rails-master.gemfile ├── vendor └── assets │ ├── stylesheets │ ├── .DS_Store │ ├── editor │ │ ├── epic-light.css │ │ └── epic-dark.css │ ├── preview │ │ ├── preview-dark.css │ │ ├── bartik.css │ │ └── github.css │ └── base │ │ └── epiceditor.css │ └── javascripts │ └── epiceditor.js.erb ├── .gitignore ├── epic-editor-rails-4.gemspec ├── README.md ├── LICENSE └── Rakefile /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec -------------------------------------------------------------------------------- /lib/epic-editor-rails.rb: -------------------------------------------------------------------------------- 1 | require "epic-editor-rails/version" 2 | require "epic-editor-rails/engine" if defined?(::Rails) -------------------------------------------------------------------------------- /gemfiles/rails-3.2.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "railties", :github => "rails/rails", :branch => "3-2-stable" -------------------------------------------------------------------------------- /gemfiles/rails-4.0.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "railties", :github => "rails/rails", :branch => "4-0-stable" -------------------------------------------------------------------------------- /gemfiles/rails-master.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "railties", :github => "rails/rails", :branch => "master" -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zethussuen/epic-editor-rails/HEAD/vendor/assets/stylesheets/.DS_Store -------------------------------------------------------------------------------- /lib/epic-editor-rails/version.rb: -------------------------------------------------------------------------------- 1 | module Epic 2 | module Editor 3 | module Rails 4 | VERSION = "0.2.4" 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | gemfiles/*.lock 5 | pkg/* 6 | bin 7 | vendor/ruby 8 | gemfiles/vendor/ruby 9 | .rbenv-version 10 | .sass-cache -------------------------------------------------------------------------------- /vendor/assets/stylesheets/editor/epic-light.css: -------------------------------------------------------------------------------- 1 | html { padding:10px; } 2 | 3 | body { 4 | border:0; 5 | background:#fcfcfc; 6 | font-family:monospace; 7 | font-size:14px; 8 | padding:10px; 9 | line-height:1.35em; 10 | margin:0; 11 | padding:0; 12 | } 13 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/editor/epic-dark.css: -------------------------------------------------------------------------------- 1 | html { padding:10px; } 2 | 3 | body { 4 | border:0; 5 | background:rgb(41,41,41); 6 | font-family:monospace; 7 | font-size:14px; 8 | padding:10px; 9 | color:#ddd; 10 | line-height:1.35em; 11 | margin:0; 12 | padding:0; 13 | } 14 | -------------------------------------------------------------------------------- /lib/epic-editor-rails/engine.rb: -------------------------------------------------------------------------------- 1 | module Epic 2 | module Editor 3 | module Rails 4 | class Engine < ::Rails::Engine 5 | if ::Rails.version >= "3.1" 6 | initializer "Precompile EpicEditor assets" do |app| 7 | app.config.assets.precompile += [ 8 | "epiceditor.js", "base/epiceditor.css", 9 | "editor/epic-dark.css", "editor/epic-light.css", 10 | "preview/bartik.css", "preview/github.css", "preview-dark.css" 11 | ] 12 | end 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /epic-editor-rails-4.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/epic-editor-rails/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["zethus"] 6 | gem.email = ["zethus.suen@gmail.com"] 7 | gem.description = "Rails 3.2/4.x asset gem for EpicEditor" 8 | gem.summary = "Rails 3.2/4.x asset gem for EpicEditor" 9 | gem.homepage = "https://github.com/zethussuen/epic-editor-rails" 10 | gem.licenses = ["MIT License"] 11 | 12 | gem.files = Dir["{app,lib,vendor}/**/*"] + ["LICENSE", "Rakefile", "README.md"] 13 | gem.name = "epic-editor-rails" 14 | gem.require_paths = ["lib"] 15 | gem.version = Epic::Editor::Rails::VERSION 16 | 17 | gem.add_dependency "railties", ">= 3.2", "< 5.0" 18 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Note: This gem is currently un-maintained. If you would like to help keep it current or take ownership, please contact me @zethussuen 2 | 3 | epic-editor-rails v0.2.4 4 | ================= 5 | Gemfile: ```gem 'epic-editor-rails'``` 6 | Install: ```$ bundle install ``` 7 | 8 | application.js: 9 | ``` 10 | //= require 'epiceditor' 11 | ``` 12 | 13 | All instructions and patterns from the upstream's README ([EpicEditor](https://github.com/OscarGodson/EpicEditor)) can be applied directly. 14 | 15 | With the exception of specifying themes, which can be accomplished along the lines of: 16 | ```erb 17 | new EpicEditor({ 18 | theme: { 19 | editor: '<%= asset_path 'editor/epic-light.css' %>', 20 | preview: '<%= asset_path 'preview/preview-dark.css' %>' 21 | } 22 | }).load(); 23 | ``` 24 | 25 | EpicEditor v0.2.3 26 | http://epiceditor.com/ 27 | 28 | 29 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/zethussuen/epic-editor-rails/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | 3 | begin 4 | require File.expand_path('../config/application', __FILE__) 5 | EpicEditorRails::Application.load_tasks 6 | rescue LoadError 7 | end 8 | 9 | # rake update[param] 10 | # param can be a tag name, a branch name, or a commit hash on https://github.com/OscarGodson/EpicEditor 11 | # It defaults to 'develop' (ie, the develop branch) 12 | task :update, [:arg1] do |t, args| 13 | version = args[:arg1] || 'develop' 14 | puts "Updating to EpicEditor #{version}" 15 | 16 | # version can be a commit hash, a branch name, or a tag name - github will handle them all the same. 17 | base_url = "https://raw.githubusercontent.com/OscarGodson/EpicEditor/#{version}/" 18 | 19 | js_url = "#{base_url}epiceditor/js/epiceditor.js" 20 | puts "copying #{js_url}" 21 | js_content = open(js_url).read 22 | 23 | puts 'Replacing base path' 24 | js_content.gsub!(/basePath: 'epiceditor'/, "basePath: ''") 25 | 26 | # Use asset pipeline version of css files. 27 | puts 'Substituting asset pipeline paths' 28 | js_content.gsub!(/'\/themes\/(\w.*)\/(\w.*)\.css'/, "'<%= asset_path(\"\\1/\\2.css\") %>'") 29 | 30 | open('./vendor/assets/javascripts/epiceditor.js.erb', 'wb') { |f| f << js_content } 31 | 32 | remote_css_base = "#{base_url}epiceditor/themes/" 33 | local_css_base = './vendor/assets/stylesheets/' 34 | css_files = [ 35 | 'base/epiceditor.css', 36 | 'editor/epic-dark.css', 37 | 'editor/epic-light.css', 38 | 'preview/bartik.css', 39 | 'preview/github.css', 40 | 'preview/preview-dark.css', 41 | ] 42 | 43 | css_files.each do |css_file| 44 | remote_path = remote_css_base + css_file 45 | local_path = local_css_base + css_file 46 | css_content = open(remote_path).read 47 | 48 | puts "copying #{remote_path}" 49 | open(local_path, 'wb') { |f| f << css_content } 50 | end 51 | end -------------------------------------------------------------------------------- /vendor/assets/stylesheets/preview/preview-dark.css: -------------------------------------------------------------------------------- 1 | html { padding:0 10px; } 2 | 3 | body { 4 | margin:0; 5 | padding:10px 0; 6 | background:#000; 7 | } 8 | 9 | #epiceditor-preview h1, 10 | #epiceditor-preview h2, 11 | #epiceditor-preview h3, 12 | #epiceditor-preview h4, 13 | #epiceditor-preview h5, 14 | #epiceditor-preview h6, 15 | #epiceditor-preview p, 16 | #epiceditor-preview blockquote { 17 | margin: 0; 18 | padding: 0; 19 | } 20 | #epiceditor-preview { 21 | background:#000; 22 | font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", Arial, sans-serif; 23 | font-size: 13px; 24 | line-height: 18px; 25 | color: #ccc; 26 | } 27 | #epiceditor-preview a { 28 | color: #fff; 29 | } 30 | #epiceditor-preview a:hover { 31 | color: #00ff00; 32 | text-decoration: none; 33 | } 34 | #epiceditor-preview a img { 35 | border: none; 36 | } 37 | #epiceditor-preview p { 38 | margin-bottom: 9px; 39 | } 40 | #epiceditor-preview h1, 41 | #epiceditor-preview h2, 42 | #epiceditor-preview h3, 43 | #epiceditor-preview h4, 44 | #epiceditor-preview h5, 45 | #epiceditor-preview h6 { 46 | color: #cdcdcd; 47 | line-height: 36px; 48 | } 49 | #epiceditor-preview h1 { 50 | margin-bottom: 18px; 51 | font-size: 30px; 52 | } 53 | #epiceditor-preview h2 { 54 | font-size: 24px; 55 | } 56 | #epiceditor-preview h3 { 57 | font-size: 18px; 58 | } 59 | #epiceditor-preview h4 { 60 | font-size: 16px; 61 | } 62 | #epiceditor-preview h5 { 63 | font-size: 14px; 64 | } 65 | #epiceditor-preview h6 { 66 | font-size: 13px; 67 | } 68 | #epiceditor-preview hr { 69 | margin: 0 0 19px; 70 | border: 0; 71 | border-bottom: 1px solid #ccc; 72 | } 73 | #epiceditor-preview blockquote { 74 | padding: 13px 13px 21px 15px; 75 | margin-bottom: 18px; 76 | font-family:georgia,serif; 77 | font-style: italic; 78 | } 79 | #epiceditor-preview blockquote:before { 80 | content:"\201C"; 81 | font-size:40px; 82 | margin-left:-10px; 83 | font-family:georgia,serif; 84 | color:#eee; 85 | } 86 | #epiceditor-preview blockquote p { 87 | font-size: 14px; 88 | font-weight: 300; 89 | line-height: 18px; 90 | margin-bottom: 0; 91 | font-style: italic; 92 | } 93 | #epiceditor-preview code, #epiceditor-preview pre { 94 | font-family: Monaco, Andale Mono, Courier New, monospace; 95 | } 96 | #epiceditor-preview code { 97 | background-color: #000; 98 | color: #f92672; 99 | padding: 1px 3px; 100 | font-size: 12px; 101 | -webkit-border-radius: 3px; 102 | -moz-border-radius: 3px; 103 | border-radius: 3px; 104 | } 105 | #epiceditor-preview pre { 106 | display: block; 107 | padding: 14px; 108 | color:#66d9ef; 109 | margin: 0 0 18px; 110 | line-height: 16px; 111 | font-size: 11px; 112 | border: 1px solid #d9d9d9; 113 | white-space: pre-wrap; 114 | word-wrap: break-word; 115 | } 116 | #epiceditor-preview pre code { 117 | background-color: #000; 118 | color:#ccc; 119 | font-size: 11px; 120 | padding: 0; 121 | } 122 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/preview/bartik.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Georgia, "Times New Roman", Times, serif; 3 | line-height: 1.5; 4 | font-size: 87.5%; 5 | word-wrap: break-word; 6 | margin: 2em; 7 | padding: 0; 8 | border: 0; 9 | outline: 0; 10 | background: #fff; 11 | } 12 | 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6 { 19 | margin: 1.0em 0 0.5em; 20 | font-weight: inherit; 21 | } 22 | 23 | h1 { 24 | font-size: 1.357em; 25 | color: #000; 26 | } 27 | 28 | h2 { 29 | font-size: 1.143em; 30 | } 31 | 32 | p { 33 | margin: 0 0 1.2em; 34 | } 35 | 36 | del { 37 | text-decoration: line-through; 38 | } 39 | 40 | tr:nth-child(odd) { 41 | background-color: #dddddd; 42 | } 43 | 44 | img { 45 | outline: 0; 46 | } 47 | 48 | code { 49 | background-color: #f2f2f2; 50 | background-color: rgba(40, 40, 0, 0.06); 51 | } 52 | 53 | pre { 54 | background-color: #f2f2f2; 55 | background-color: rgba(40, 40, 0, 0.06); 56 | margin: 10px 0; 57 | overflow: hidden; 58 | padding: 15px; 59 | white-space: pre-wrap; 60 | } 61 | 62 | pre code { 63 | font-size: 100%; 64 | background-color: transparent; 65 | } 66 | 67 | blockquote { 68 | background: #f7f7f7; 69 | border-left: 1px solid #bbb; 70 | font-style: italic; 71 | margin: 1.5em 10px; 72 | padding: 0.5em 10px; 73 | } 74 | 75 | blockquote:before { 76 | color: #bbb; 77 | content: "\201C"; 78 | font-size: 3em; 79 | line-height: 0.1em; 80 | margin-right: 0.2em; 81 | vertical-align: -.4em; 82 | } 83 | 84 | blockquote:after { 85 | color: #bbb; 86 | content: "\201D"; 87 | font-size: 3em; 88 | line-height: 0.1em; 89 | vertical-align: -.45em; 90 | } 91 | 92 | blockquote > p:first-child { 93 | display: inline; 94 | } 95 | 96 | table { 97 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 98 | border: 0; 99 | border-spacing: 0; 100 | font-size: 0.857em; 101 | margin: 10px 0; 102 | width: 100%; 103 | } 104 | 105 | table table { 106 | font-size: 1em; 107 | } 108 | 109 | table tr th { 110 | background: #757575; 111 | background: rgba(0, 0, 0, 0.51); 112 | border-bottom-style: none; 113 | } 114 | 115 | table tr th, 116 | table tr th a, 117 | table tr th a:hover { 118 | color: #FFF; 119 | font-weight: bold; 120 | } 121 | 122 | table tbody tr th { 123 | vertical-align: top; 124 | } 125 | 126 | tr td, 127 | tr th { 128 | padding: 4px 9px; 129 | border: 1px solid #fff; 130 | text-align: left; /* LTR */ 131 | } 132 | 133 | tr:nth-child(odd) { 134 | background: #e4e4e4; 135 | background: rgba(0, 0, 0, 0.105); 136 | } 137 | 138 | tr, 139 | tr:nth-child(even) { 140 | background: #efefef; 141 | background: rgba(0, 0, 0, 0.063); 142 | } 143 | 144 | a { 145 | color: #0071B3; 146 | } 147 | 148 | a:hover, 149 | a:focus { 150 | color: #018fe2; 151 | } 152 | 153 | a:active { 154 | color: #23aeff; 155 | } 156 | 157 | a:link, 158 | a:visited { 159 | text-decoration: none; 160 | } 161 | 162 | a:hover, 163 | a:active, 164 | a:focus { 165 | text-decoration: underline; 166 | } 167 | 168 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/preview/github.css: -------------------------------------------------------------------------------- 1 | html { padding:0 10px; } 2 | 3 | body { 4 | margin:0; 5 | padding:0; 6 | background:#fff; 7 | } 8 | 9 | #epiceditor-wrapper{ 10 | background:white; 11 | } 12 | 13 | #epiceditor-preview{ 14 | padding-top:10px; 15 | padding-bottom:10px; 16 | font-family: Helvetica,arial,freesans,clean,sans-serif; 17 | font-size:13px; 18 | line-height:1.6; 19 | } 20 | 21 | #epiceditor-preview>*:first-child{ 22 | margin-top:0!important; 23 | } 24 | 25 | #epiceditor-preview>*:last-child{ 26 | margin-bottom:0!important; 27 | } 28 | 29 | #epiceditor-preview a{ 30 | color:#4183C4; 31 | text-decoration:none; 32 | } 33 | 34 | #epiceditor-preview a:hover{ 35 | text-decoration:underline; 36 | } 37 | 38 | #epiceditor-preview h1, 39 | #epiceditor-preview h2, 40 | #epiceditor-preview h3, 41 | #epiceditor-preview h4, 42 | #epiceditor-preview h5, 43 | #epiceditor-preview h6{ 44 | margin:20px 0 10px; 45 | padding:0; 46 | font-weight:bold; 47 | -webkit-font-smoothing:antialiased; 48 | } 49 | 50 | #epiceditor-preview h1 tt, 51 | #epiceditor-preview h1 code, 52 | #epiceditor-preview h2 tt, 53 | #epiceditor-preview h2 code, 54 | #epiceditor-preview h3 tt, 55 | #epiceditor-preview h3 code, 56 | #epiceditor-preview h4 tt, 57 | #epiceditor-preview h4 code, 58 | #epiceditor-preview h5 tt, 59 | #epiceditor-preview h5 code, 60 | #epiceditor-preview h6 tt, 61 | #epiceditor-preview h6 code{ 62 | font-size:inherit; 63 | } 64 | 65 | #epiceditor-preview h1{ 66 | font-size:28px; 67 | color:#000; 68 | } 69 | 70 | #epiceditor-preview h2{ 71 | font-size:24px; 72 | border-bottom:1px solid #ccc; 73 | color:#000; 74 | } 75 | 76 | #epiceditor-preview h3{ 77 | font-size:18px; 78 | } 79 | 80 | #epiceditor-preview h4{ 81 | font-size:16px; 82 | } 83 | 84 | #epiceditor-preview h5{ 85 | font-size:14px; 86 | } 87 | 88 | #epiceditor-preview h6{ 89 | color:#777; 90 | font-size:14px; 91 | } 92 | 93 | #epiceditor-preview p, 94 | #epiceditor-preview blockquote, 95 | #epiceditor-preview ul, 96 | #epiceditor-preview ol, 97 | #epiceditor-preview dl, 98 | #epiceditor-preview li, 99 | #epiceditor-preview table, 100 | #epiceditor-preview pre{ 101 | margin:15px 0; 102 | } 103 | 104 | #epiceditor-preview hr{ 105 | background:transparent url('../../images/modules/pulls/dirty-shade.png') repeat-x 0 0; 106 | border:0 none; 107 | color:#ccc; 108 | height:4px; 109 | padding:0; 110 | } 111 | 112 | #epiceditor-preview>h2:first-child, 113 | #epiceditor-preview>h1:first-child, 114 | #epiceditor-preview>h1:first-child+h2, 115 | #epiceditor-preview>h3:first-child, 116 | #epiceditor-preview>h4:first-child, 117 | #epiceditor-preview>h5:first-child, 118 | #epiceditor-preview>h6:first-child{ 119 | margin-top:0; 120 | padding-top:0; 121 | } 122 | 123 | #epiceditor-preview h1+p, 124 | #epiceditor-preview h2+p, 125 | #epiceditor-preview h3+p, 126 | #epiceditor-preview h4+p, 127 | #epiceditor-preview h5+p, 128 | #epiceditor-preview h6+p{ 129 | margin-top:0; 130 | } 131 | 132 | #epiceditor-preview li p.first{ 133 | display:inline-block; 134 | } 135 | 136 | #epiceditor-preview ul, 137 | #epiceditor-preview ol{ 138 | padding-left:30px; 139 | } 140 | 141 | #epiceditor-preview ul li>:first-child, 142 | #epiceditor-preview ol li>:first-child{ 143 | margin-top:0; 144 | } 145 | 146 | #epiceditor-preview ul li>:last-child, 147 | #epiceditor-preview ol li>:last-child{ 148 | margin-bottom:0; 149 | } 150 | 151 | #epiceditor-preview dl{ 152 | padding:0; 153 | } 154 | 155 | #epiceditor-preview dl dt{ 156 | font-size:14px; 157 | font-weight:bold; 158 | font-style:italic; 159 | padding:0; 160 | margin:15px 0 5px; 161 | } 162 | 163 | #epiceditor-preview dl dt:first-child{ 164 | padding:0; 165 | } 166 | 167 | #epiceditor-preview dl dt>:first-child{ 168 | margin-top:0; 169 | } 170 | 171 | #epiceditor-preview dl dt>:last-child{ 172 | margin-bottom:0; 173 | } 174 | 175 | #epiceditor-preview dl dd{ 176 | margin:0 0 15px; 177 | padding:0 15px; 178 | } 179 | 180 | #epiceditor-preview dl dd>:first-child{ 181 | margin-top:0; 182 | } 183 | 184 | #epiceditor-preview dl dd>:last-child{ 185 | margin-bottom:0; 186 | } 187 | 188 | #epiceditor-preview blockquote{ 189 | border-left:4px solid #DDD; 190 | padding:0 15px; 191 | color:#777; 192 | } 193 | 194 | #epiceditor-preview blockquote>:first-child{ 195 | margin-top:0; 196 | } 197 | 198 | #epiceditor-preview blockquote>:last-child{ 199 | margin-bottom:0; 200 | } 201 | 202 | #epiceditor-preview table{ 203 | padding:0; 204 | border-collapse: collapse; 205 | border-spacing: 0; 206 | font-size: 100%; 207 | font: inherit; 208 | } 209 | 210 | #epiceditor-preview table tr{ 211 | border-top:1px solid #ccc; 212 | background-color:#fff; 213 | margin:0; 214 | padding:0; 215 | } 216 | 217 | #epiceditor-preview table tr:nth-child(2n){ 218 | background-color:#f8f8f8; 219 | } 220 | 221 | #epiceditor-preview table tr th{ 222 | font-weight:bold; 223 | } 224 | 225 | #epiceditor-preview table tr th, 226 | #epiceditor-preview table tr td{ 227 | border:1px solid #ccc; 228 | margin:0; 229 | padding:6px 13px; 230 | } 231 | 232 | #epiceditor-preview table tr th>:first-child, 233 | #epiceditor-preview table tr td>:first-child{ 234 | margin-top:0; 235 | } 236 | 237 | #epiceditor-preview table tr th>:last-child, 238 | #epiceditor-preview table tr td>:last-child{ 239 | margin-bottom:0; 240 | } 241 | 242 | #epiceditor-preview img{ 243 | max-width:100%; 244 | } 245 | 246 | #epiceditor-preview span.frame{ 247 | display:block; 248 | overflow:hidden; 249 | } 250 | 251 | #epiceditor-preview span.frame>span{ 252 | border:1px solid #ddd; 253 | display:block; 254 | float:left; 255 | overflow:hidden; 256 | margin:13px 0 0; 257 | padding:7px; 258 | width:auto; 259 | } 260 | 261 | #epiceditor-preview span.frame span img{ 262 | display:block; 263 | float:left; 264 | } 265 | 266 | #epiceditor-preview span.frame span span{ 267 | clear:both; 268 | color:#333; 269 | display:block; 270 | padding:5px 0 0; 271 | } 272 | 273 | #epiceditor-preview span.align-center{ 274 | display:block; 275 | overflow:hidden; 276 | clear:both; 277 | } 278 | 279 | #epiceditor-preview span.align-center>span{ 280 | display:block; 281 | overflow:hidden; 282 | margin:13px auto 0; 283 | text-align:center; 284 | } 285 | 286 | #epiceditor-preview span.align-center span img{ 287 | margin:0 auto; 288 | text-align:center; 289 | } 290 | 291 | #epiceditor-preview span.align-right{ 292 | display:block; 293 | overflow:hidden; 294 | clear:both; 295 | } 296 | 297 | #epiceditor-preview span.align-right>span{ 298 | display:block; 299 | overflow:hidden; 300 | margin:13px 0 0; 301 | text-align:right; 302 | } 303 | 304 | #epiceditor-preview span.align-right span img{ 305 | margin:0; 306 | text-align:right; 307 | } 308 | 309 | #epiceditor-preview span.float-left{ 310 | display:block; 311 | margin-right:13px; 312 | overflow:hidden; 313 | float:left; 314 | } 315 | 316 | #epiceditor-preview span.float-left span{ 317 | margin:13px 0 0; 318 | } 319 | 320 | #epiceditor-preview span.float-right{ 321 | display:block; 322 | margin-left:13px; 323 | overflow:hidden; 324 | float:right; 325 | } 326 | 327 | #epiceditor-preview span.float-right>span{ 328 | display:block; 329 | overflow:hidden; 330 | margin:13px auto 0; 331 | text-align:right; 332 | } 333 | 334 | #epiceditor-preview code, 335 | #epiceditor-preview tt{ 336 | margin:0 2px; 337 | padding:0 5px; 338 | white-space:nowrap; 339 | border:1px solid #eaeaea; 340 | background-color:#f8f8f8; 341 | border-radius:3px; 342 | } 343 | 344 | #epiceditor-preview pre>code{ 345 | margin:0; 346 | padding:0; 347 | white-space:pre; 348 | border:none; 349 | background:transparent; 350 | } 351 | 352 | #epiceditor-preview .highlight pre, 353 | #epiceditor-preview pre{ 354 | background-color:#f8f8f8; 355 | border:1px solid #ccc; 356 | font-size:13px; 357 | line-height:19px; 358 | overflow:auto; 359 | padding:6px 10px; 360 | border-radius:3px; 361 | } 362 | 363 | #epiceditor-preview pre code, 364 | #epiceditor-preview pre tt{ 365 | background-color:transparent; 366 | border:none; 367 | } 368 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/base/epiceditor.css: -------------------------------------------------------------------------------- 1 | html, body, iframe, div { 2 | margin:0; 3 | padding:0; 4 | } 5 | 6 | #epiceditor-utilbar { 7 | position:fixed; 8 | bottom:10px; 9 | right:10px; 10 | } 11 | 12 | #epiceditor-utilbar button { 13 | display:block; 14 | float:left; 15 | width:30px; 16 | height:30px; 17 | border:none; 18 | background:none; 19 | } 20 | 21 | #epiceditor-utilbar button.epiceditor-toggle-preview-btn { 22 | background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAf9JREFUeNrsVu1twjAQLZmAEcIGYYKGDWACkgkSkPiQ+CifAoEgyQTQDegEpROQDZoRMgH0HToj1yIglKr90Vg62T6f/e4d9gu50+n09BctlwFnwP8TOAgCA10VRr2ZELaHhbBXx3HCVMC+71voXmD6g4Qi2NB13e1DwAAkZhtmmKYRcxsJhHeBPc8bMMufbDU0PxF4vV4TSyth8477csI6lTV/a71er9tioonBarXaIAmLErliNWyqoM8nrJPpHFNLWLcI4xvj5XKpMo2ZgcvzIvs+75S0wKwPPB/CnpWXsG00Gra2WCwshekOVoC9Sb6IGN1ge2HNsWK+B0iJqxAL5oSpYeDJJW02mxVYLAWSGfDtebylA68Bc4wh+ahK5PcxLh6PR5GUpym/iTOfz89PqNVqhRI4iQf1/o174HNMVYDSGeTDmXQ3znogCGrtdpsYVBhER1aH2Wzm8iE7UR74DMTWGNxUQWmNYqTEzq8APoo9sJ8wKoR5eU7T6VQVjZAvx4YvDJWt1Ol0QsTqkppF8EW8/12OhTnSpT2LCe2/KiCTyUQVkJgPuwgb6XG32w05Xui4q0imLLNDxA/uSuZ4PNaZqZlSsejDYfd6veihj8RoNDK5XOUHAen3Dfr9/j7VZxEJ6AwuxCCvhMTM7oNAARhl/0Ay4Az419qXAAMAfBdK7281j6YAAAAASUVORK5CYII=); 23 | } 24 | 25 | #epiceditor-utilbar button.epiceditor-toggle-edit-btn { 26 | background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnVJREFUeNrsV+lt4lAQNtEWAB2QCtZ0ABWsqYB1BU5AgDgkQOISl9cV4FQQtgLYCuKtIN4K4gogM9H3pMmLTTh24c+O9An7+XnOb8aP1G63M64hN8aV5GqGvxyyyfO8Y3S6hDtc2wTfcZyLRPwdvxEbPSviIwjIRtO49s9ONSRLMIGvwkBI+EPYEEqyQmcZdl2Xo3BgcJ90xPUGDh1veLFYWCBK9oQ6P4BgiXVO6fUjg5zCJcE6kVxsLEN4QTlWzO5yuRwlGp7P55zOxz1RRqK2SfKD8BvOG4IHxUqlEnxop9lsZpITa0KWndGwIeQIXswzHbynpK2xzjXbeBfxdDrlhbUWjYyuiJS9fBJxgL3PItKsprNQrVaD1GQyYUVP2oYelDziPoPnT5+k2QajleEC3usI/exM7oYiXor0hpxS8qhLv5FIVVrcBwkp5ueclbS27qNMvkj7kg3nZX1qtVqAaSUN5OGUwn2M4RUb3263plh700E6I7wTKn1suCAW3PF4zL1r1Ov1SBhXZLEJFqGTQCq5N1BZIp3s+DOi5fXcG7lGo1Ea5DIFSWzcq7a4R6uYqJmlkSqHoeGKeq+w905MtGKj0Yje9TE5ID9pqictQQwmXTfh82cIJ0NML0d0QY8MdhMn13A4zENB0hAJEYn6EnGL3MZ0hsyG3Ww2g70jU8lgMOhqHicJz+KfovVkz3J5/FardfhBgDZzS90SeoJ8cXjQJlUAEmZUCx30kYiTfr9vgFRc72+ChCHSzNH+Qgk+fA7b7fZZJ5AAiIRhT4zUv3/Y07LiaPW9yPE2L5jrI/p/d7wVEZe0U8bJkvr/F+ZS8irAAIorRozUvI0gAAAAAElFTkSuQmCC); 27 | } 28 | 29 | #epiceditor-utilbar button.epiceditor-fullscreen-btn { 30 | background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAY5JREFUeNrsV8tthDAQhYgCtoV04BKghK2En5AQBw4IITjwq2RLgBJcwpawHZAxMpGDYGwTFPaQkUbG1sybj58NmNM0GVfIh3GRXBbYEid932N9d1zXHWWAGAb4m+9VsUA0H5SubKkKIGA4qyUC2qKBxSCe541HKln7dV2nVbHRtq0Nw7CXGNtz/jzwqjZ5odtqTOagQRC82KRpGkcS/L2Ok7lXZV3Xp7Q6DMNR2mqNthMhKXLqzcUCc6Wgn3wU1wlX1PboHs8tjaLoyVtLT7JFK2ZZM6CZvWyE+X1Voaj3la3DMA63epGqqm4wfyCBH8xmz1+Z1WVZ2vyqU2HvHtv9OI4PsRpjL91YV2a7pcB8onmOifYFUhSFLQCTnQtkDpokyYv73JBtcAQsA3zGTXJBEgNXgrF3CcrBZGwnC+4uqxHnH+zN8/ybvexZwvZNhlsHbrt5CyCgDtu1XosUe58K4kuOF9EKnKYp20eVrxDUJssyreM0bDg4kIw0EfCbftvqQ6KKYf7/wvyVfAkwAMjqoFSaWbrBAAAAAElFTkSuQmCC); 31 | } 32 | 33 | @media 34 | only screen and (-webkit-min-device-pixel-ratio: 2), 35 | only screen and ( min--moz-device-pixel-ratio: 2), 36 | only screen and ( -o-min-device-pixel-ratio: 2/1), 37 | only screen and ( min-device-pixel-ratio: 2), 38 | only screen and ( min-resolution: 192dpi), 39 | only screen and ( min-resolution: 2dppx) { 40 | #epiceditor-utilbar button.epiceditor-toggle-preview-btn { 41 | background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAABrVBMVEWIiIiHh4eIiIiJiYmEhISEhISEhISMjIyNjY2MjIyNjY2MjIyFhYWDg4OFhYWKioqKioqBgYGJiYmJiYmJiYmKioqKioqFhYWIiIiJiYmIiIiJiYmIiIiGhoaIiIiGhoaIiIiHh4eHh4eGhoaFhYWKioqFhYWEhISFhYWEhISDg4ONjY2NjY2MjIyHh4eMjIyLi4uCgoKCgoKCgoKBgYGBgYGOjo6Ojo6BgYGPj4+Ojo6Hh4eNjY2Pj4+BgYGNjY2BgYGHh4eQkJCNjY2FhYWHh4eDg4OQkJCQkJCAgICCgoKAgICAgICQkJCQkJCQkJCAgICQkJCAgICQkJCFhYWHh4eHh4eGhoaKioqGhoaMjIyDg4OEhISDg4OEhISDg4OEhISKioqBgYGJiYmKioqKioqJiYmMjIyEhISCgoKGhoaLi4uPj4+Pj4+BgYGKioqDg4OGhoaGhoaMjIyHh4eEhISEhISDg4OIiIiJiYmGhoaHh4eJiYkAAACAgICFhYWQkJCIiIiEhISLi4uDg4OHh4eGhoaCgoKBgYGJiYmMjIyOjo6KioqNjY2Pj4+o6Lr0AAAAfnRSTlPfvxDPEL8gv5/Pr3AQ3++vn8+/cCCA34/vj8/vMJ+vgJ+vYN/fMIBAv4/v38/v39+PgBBg74DPgGAwMHDvrzBQj1AggJ8wIHC/IM9gjxBQgO+vv89Qz0C/70Bgz89ggIDvYN/fII8wIHDvMJ+/QBBAMBDPnxBgML9AEI/vQACT8cYwAAACXElEQVR42u2WV1caURRGJ73H9J7YW+y99957ARREURAFQRFEOFGMkybxN+eec2cWUwBnrTwli/1yvvnm7PvEuoMAf0FGzsj/nDwxOTUzdyoxNzM1OWFU7h0aPtUxPNRrQPZ0XKagw3OF7Nm9TMOuJ43cQmpavSWV3HRhgKakcmvjhSEaW/VyV/tvg7R3aeU+9ULZ/fLEQ/ndMvXbPrV88EvBvQdOMCsLMzjNd5TFgSTr3SpWOCuUTYWTVVV6m+Sdr0qqqVGxw6pqXUPyZlziFaU9Vi3HVSyzag+D/YlcbXLZLm+85AsOgMK4hkIABz/YkSVVdpS3fnKevQCIYgCcGqKslGd0g3dbIIR5fP8cZCkMdOANWeSLEJZklt5StxEWcmLIuw+AHGE+YiEWE67jGxnlO8xv8OGTEBGRRSACmNtYyBUjgcCCKJPLqjYMAeD04ENEGIjQ7AGikuUFNhdF8Vogj0T5bDyqEjh55AwI4N7/hmRTe4zRxEM+ZuKYFSYeEP9HzPtuEFjm9pKf9W5KQLbKhSVMbkymfHL90i+s/wR5PM9iCaYiLOcLTogCrKEIYwkLD19T25/4bR+unSG3bkOQwiG1QZfV6gryBaqL5c01XCCZ9lbOCOvNUpouUOGishSK+dpKUHMZ2M6Jz7ZHNEO+hOoLUWVZZROx6a8hn+VcRWh1EOtBUuhcPiy+pBdg3fZ3DaOj2ma7LtXVW1uj0XVqTW2aS9/bUP8jJfUN3is+N97mp8nV9Wavka9kZ/e6zuzuNP59Hhkbn53+QkzPjo+NZP6TZOSM/L/LfwAJ69Ba3fXmtwAAAABJRU5ErkJggg==); 42 | background-size: 30px 30px; 43 | } 44 | 45 | #epiceditor-utilbar button.epiceditor-toggle-edit-btn { 46 | background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAACNFBMVEWIiIiHh4eIiIiGhoaEhISDg4OCgoKDg4OMjIyNjY2MjIyMjIyLi4uMjIyKioqCgoKNjY2Ojo6Dg4ONjY2FhYWFhYWCgoKKioqKioqLi4uLi4uMjIyLi4uFhYWKioqKioqEhISJiYmJiYmEhISEhISFhYWEhISGhoaHh4eJiYmNjY2Hh4eKioqDg4ODg4OCgoKOjo6Ojo6Dg4OOjo6BgYGOjo6AgICCgoKNjY2Dg4OPj4+BgYGBgYGAgICOjo6BgYGOjo6AgICOjo6BgYGBgYGAgICPj4+QkJCQkJCQkJCQkJCPj4+NjY2IiIiIiIiFhYWFhYWEhISEhISGhoaDg4OGhoaDg4OHh4eDg4OCgoKMjIyLi4uLi4uKioqNjY2KioqNjY2NjY2NjY2Dg4OKioqGhoaJiYmIiIiGhoaFhYWEhISFhYWFhYWOjo6Pj4+FhYWGhoaPj4+Hh4eMjIyMjIyMjIyJiYmIiIiIiIiOjo6Ojo6CgoKBgYGPj4+QkJCHh4eHh4eEhISIiIiDg4OEhISDg4ODg4OLi4uHh4eEhISMjIyFhYWGhoaHh4eOjo6IiIiKioqKioqMjIyGhoaIiIiJiYmIiIiFhYWGhoaGhoaHh4eKioqJiYmLi4uIiIiGhoaJiYmHh4eGhoaGhoaJiYmFhYWKioqPj4+IiIiGhoYAAACAgICCgoKQkJCPj4+BgYGDg4OFhYWHh4eOjo6JiYmGhoaLi4uEhISIiIiNjY2MjIyKioqRGRgVAAAAq3RSTlNQz7+Pz4AQr4/vIK+Av99QICBQzyCv37/PcM+f378QgHAwgIBgnxBQMK+PgFCf34Bwn0BQv2Ag7zBwEFDvj4CAv1DvYK/f398gUICA39+fYHAwQHAQn78gz0DfYO8wgO+/YECPIK9gz89Av8/f31AwgK9A72AwIGCAMM+PICCvrxCfr+/fYCAgYO9QgGBQjyCPYM8QcM8wEEAgn58Qn+8w7+/fv0DvQEBA7wCxres+AAAC50lEQVR42u2XVUMbQRSFU3d3d3d3d8EpUKRoKbS40+LuDsEtQOFQKA2FEvhz3XsnS5IlJLB9a/s95J49M99sXpJsNPgD/st/oZwxJJGhTn4+RLxQJ78k9xnUyU+HJV6pk6OGiXR1ciy5sZizfPMrwzl9mIiak5x6/sLnUZmH9+9eqqAQCftyakXkqFUq7MpX6JbW2WBHDnxtmJUAIMiGfD3AYINArDdsCppNfmewxQqx4aRVObFm0ibLAW+aNYkz5ZL4SdusRIkI8SVKOcFyp/cu5ftYA6ySc4Kl3DZuxs4dhfAZV+CDQtNFm0lWuLsBFPoqXF9g9bjSZrlqypwqAC1TClqAtYprIVfLjT+f0gfAXyn7oY9GNZ/KiWVumX17OYYAIUp3O+AnDu7bZqxOk5zU+ZOpPwD0UABNBaEAaPYACBVCZ5Ik14vlg5ClVjFpC0NZ6lplGa0nxN2gSZkg2vtB9FOmKDUNCyemcTStMXXcpmh4yGUl5ToAORMOaGiflhtkoRKCfq41yTw+GFsHKeeIRUfwwbwKOo/eIATi3GQNivREVxyIZsqeADL1+swuvZEiAJ4UmsEUsVEODRAnNp2iulzekrVAbyJLPrYctJTJ7nGQjI7uMULXBIBjIyQWUWLeAGik0E39sQGKYU0QMmp1vGnADSjj0EFtk5uOjuKzOtgok8r34rxasMzEjDFhjdDJNsE7u2VXh9oYDgNllp/n8IgfzJbwXhq9zlRu5soZWtFFl/Zy8SkaljK0R6inJTEinLQo5aSFE889kkqUWvsCdM37ZcnHYnrNBhably5QyoJDtFuJK1xMF3mHgVlkHM2e4eYB02Xxfts/NwXBuSMWLIG7sTmbb/+Hzj3fy1wuQD6N3DMX5/g0VHDDQ3aXAR4jXsEb5/co9fbLN2KdlJbO/zmM5a0qH+KukXxOnfzoO5GmTr5M7mOoktP4xrfUyffIvQ118pNBiTvq5AeDxNV//W+CFX4Doxg1lHC8FNUAAAAASUVORK5CYII=); 47 | background-size: 30px 30px; 48 | } 49 | 50 | #epiceditor-utilbar button.epiceditor-fullscreen-btn { 51 | background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAAA5FBMVEWPj4+JiYmJiYmCgoKGhoaPj4+Li4uEhISAgICNjY2Li4uNjY2QkJCFhYWEhISLi4uHh4eHh4eIiIiHh4eGhoaHh4eEhISOjo6EhISJiYmFhYWJiYmFhYWNjY2NjY2Hh4eOjo6CgoKGhoaKioqKioqJiYmKioqDg4ODg4OBgYGGhoaKioqKioqLi4uMjIyMjIyIiIiIiIiMjIyOjo6GhoaDg4ONjY2EhISPj4+CgoIAAACJiYmHh4eIiIiFhYWCgoKPj4+Li4uAgICKioqEhISGhoaBgYGOjo6MjIyQkJCNjY2Dg4O2Jy/OAAAAO3RSTlMQz0DPEHDPz0DvMDBAQBBAQBAQz88wMM/v7zAw788Q7xAQQM9AEBDPEEAwMO/v7zDvMEBA70BAQEBAAAENmbYAAAG1SURBVHja7dftM0JREMdxD5GkEEqKpBJRSc8rQq7Q////2Dnm3t85jtkTmmGG75t7Z3c/t9fN0Tf6x1/Dz0ZrZGZf/BJ8a9QjM/vCwks9Px5aBcslC+P3nPVmi8dcczrcHHM2flug1CHRYcoYNd0YFlrAL1yHqPOC9g9IdbAfjHAjYFjoT+FI1MfRiAM/cZdEl0+oVidVvRaMcAOMgJWGBUYSVhrWjdfvjKqrq1Vzsu7Cy1Vo7XXZhYuj0ahwfHY+sjo/Oy7woyhitkTQsESsZawstG6VlvDCfIlUmfSVVjpDqtL8goBLbKFhsRcwalxcB100CDkxLLQbJxKwpvb3At7Y2iRuJzd4V26HuM2tDQkPWMOamu1Awkeetx2qtDy/lvZaCW173pGIWetA/xBbF+ZgiVgjGcdutLJ7xO1l9VnMiWGhJdzl4vx4CNpN+ifJXUy7RPEuZ2C1AIY1NG5kHI4Dx8MynnBtovYkqHzi25OyP8ONiKFh3djQsCIecn2i/lBvMU+UXxwi3HyE832jvD0RsLuP8CN3Oh0+feRmixE+g7CdLb43WiEz++Jb+M//x/gB/ArCl0G4nsFuEQAAAABJRU5ErkJggg==); 52 | background-size: 30px 30px; 53 | } 54 | } 55 | 56 | #epiceditor-utilbar button:last-child { 57 | margin-left:15px; 58 | } 59 | 60 | #epiceditor-utilbar button:hover { 61 | cursor:pointer; 62 | } 63 | 64 | .epiceditor-edit-mode #epiceditor-utilbar button.epiceditor-toggle-edit-btn { 65 | display:none; 66 | } 67 | 68 | .epiceditor-preview-mode #epiceditor-utilbar button.epiceditor-toggle-preview-btn { 69 | display:none; 70 | } 71 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/epiceditor.js.erb: -------------------------------------------------------------------------------- 1 | /** 2 | * EpicEditor - An Embeddable JavaScript Markdown Editor (https://github.com/OscarGodson/EpicEditor) 3 | * Copyright (c) 2011-2012, Oscar Godson. (MIT Licensed) 4 | */ 5 | 6 | (function (window, undefined) { 7 | /** 8 | * Applies attributes to a DOM object 9 | * @param {object} context The DOM obj you want to apply the attributes to 10 | * @param {object} attrs A key/value pair of attributes you want to apply 11 | * @returns {undefined} 12 | */ 13 | function _applyAttrs(context, attrs) { 14 | for (var attr in attrs) { 15 | if (attrs.hasOwnProperty(attr)) { 16 | context.setAttribute(attr, attrs[attr]); 17 | } 18 | } 19 | } 20 | 21 | /** 22 | * Applies styles to a DOM object 23 | * @param {object} context The DOM obj you want to apply the attributes to 24 | * @param {object} attrs A key/value pair of attributes you want to apply 25 | * @returns {undefined} 26 | */ 27 | function _applyStyles(context, attrs) { 28 | for (var attr in attrs) { 29 | if (attrs.hasOwnProperty(attr)) { 30 | context.style[attr] = attrs[attr]; 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Returns a DOM objects computed style 37 | * @param {object} el The element you want to get the style from 38 | * @param {string} styleProp The property you want to get from the element 39 | * @returns {string} Returns a string of the value. If property is not set it will return a blank string 40 | */ 41 | function _getStyle(el, styleProp) { 42 | var x = el 43 | , y = null; 44 | if (window.getComputedStyle) { 45 | y = document.defaultView.getComputedStyle(x, null).getPropertyValue(styleProp); 46 | } 47 | else if (x.currentStyle) { 48 | y = x.currentStyle[styleProp]; 49 | } 50 | return y; 51 | } 52 | 53 | /** 54 | * Saves the current style state for the styles requested, then applies styles 55 | * to overwrite the existing one. The old styles are returned as an object so 56 | * you can pass it back in when you want to revert back to the old style 57 | * @param {object} el The element to get the styles of 58 | * @param {string} type Can be "save" or "apply". apply will just apply styles you give it. Save will write styles 59 | * @param {object} styles Key/value style/property pairs 60 | * @returns {object} 61 | */ 62 | function _saveStyleState(el, type, styles) { 63 | var returnState = {} 64 | , style; 65 | if (type === 'save') { 66 | for (style in styles) { 67 | if (styles.hasOwnProperty(style)) { 68 | returnState[style] = _getStyle(el, style); 69 | } 70 | } 71 | // After it's all done saving all the previous states, change the styles 72 | _applyStyles(el, styles); 73 | } 74 | else if (type === 'apply') { 75 | _applyStyles(el, styles); 76 | } 77 | return returnState; 78 | } 79 | 80 | /** 81 | * Gets an elements total width including it's borders and padding 82 | * @param {object} el The element to get the total width of 83 | * @returns {int} 84 | */ 85 | function _outerWidth(el) { 86 | var b = parseInt(_getStyle(el, 'border-left-width'), 10) + parseInt(_getStyle(el, 'border-right-width'), 10) 87 | , p = parseInt(_getStyle(el, 'padding-left'), 10) + parseInt(_getStyle(el, 'padding-right'), 10) 88 | , w = el.offsetWidth 89 | , t; 90 | // For IE in case no border is set and it defaults to "medium" 91 | if (isNaN(b)) { b = 0; } 92 | t = b + p + w; 93 | return t; 94 | } 95 | 96 | /** 97 | * Gets an elements total height including it's borders and padding 98 | * @param {object} el The element to get the total width of 99 | * @returns {int} 100 | */ 101 | function _outerHeight(el) { 102 | var b = parseInt(_getStyle(el, 'border-top-width'), 10) + parseInt(_getStyle(el, 'border-bottom-width'), 10) 103 | , p = parseInt(_getStyle(el, 'padding-top'), 10) + parseInt(_getStyle(el, 'padding-bottom'), 10) 104 | , w = parseInt(_getStyle(el, 'height'), 10) 105 | , t; 106 | // For IE in case no border is set and it defaults to "medium" 107 | if (isNaN(b)) { b = 0; } 108 | t = b + p + w; 109 | return t; 110 | } 111 | 112 | /** 113 | * Inserts a tag specifically for CSS 114 | * @param {string} path The path to the CSS file 115 | * @param {object} context In what context you want to apply this to (document, iframe, etc) 116 | * @param {string} id An id for you to reference later for changing properties of the 117 | * @returns {undefined} 118 | */ 119 | function _insertCSSLink(path, context, id) { 120 | id = id || ''; 121 | var headID = context.getElementsByTagName("head")[0] 122 | , cssNode = context.createElement('link'); 123 | 124 | _applyAttrs(cssNode, { 125 | type: 'text/css' 126 | , id: id 127 | , rel: 'stylesheet' 128 | , href: path 129 | , name: path 130 | , media: 'screen' 131 | }); 132 | 133 | headID.appendChild(cssNode); 134 | } 135 | 136 | // Simply replaces a class (o), to a new class (n) on an element provided (e) 137 | function _replaceClass(e, o, n) { 138 | e.className = e.className.replace(o, n); 139 | } 140 | 141 | // Feature detects an iframe to get the inner document for writing to 142 | function _getIframeInnards(el) { 143 | return el.contentDocument || el.contentWindow.document; 144 | } 145 | 146 | // Grabs the text from an element and preserves whitespace 147 | function _getText(el) { 148 | var theText; 149 | // Make sure to check for type of string because if the body of the page 150 | // doesn't have any text it'll be "" which is falsey and will go into 151 | // the else which is meant for Firefox and shit will break 152 | if (typeof document.body.innerText == 'string') { 153 | theText = el.innerText; 154 | } 155 | else { 156 | // First replace
s before replacing the rest of the HTML 157 | theText = el.innerHTML.replace(/
/gi, "\n"); 158 | // Now we can clean the HTML 159 | theText = theText.replace(/<(?:.|\n)*?>/gm, ''); 160 | // Now fix HTML entities 161 | theText = theText.replace(/</gi, '<'); 162 | theText = theText.replace(/>/gi, '>'); 163 | } 164 | return theText; 165 | } 166 | 167 | function _setText(el, content) { 168 | // Don't convert lt/gt characters as HTML when viewing the editor window 169 | // TODO: Write a test to catch regressions for this 170 | content = content.replace(//g, '>'); 172 | content = content.replace(/\n/g, '
'); 173 | 174 | // Make sure to there aren't two spaces in a row (replace one with  ) 175 | // If you find and replace every space with a   text will not wrap. 176 | // Hence the name (Non-Breaking-SPace). 177 | // TODO: Probably need to test this somehow... 178 | content = content.replace(/
\s/g, '
 ') 179 | content = content.replace(/\s\s\s/g, '   ') 180 | content = content.replace(/\s\s/g, '  ') 181 | content = content.replace(/^ /, ' ') 182 | 183 | el.innerHTML = content; 184 | return true; 185 | } 186 | 187 | /** 188 | * Converts the 'raw' format of a file's contents into plaintext 189 | * @param {string} content Contents of the file 190 | * @returns {string} the sanitized content 191 | */ 192 | function _sanitizeRawContent(content) { 193 | // Get this, 2 spaces in a content editable actually converts to: 194 | // 0020 00a0, meaning, "space no-break space". So, manually convert 195 | // no-break spaces to spaces again before handing to marked. 196 | // Also, WebKit converts no-break to unicode equivalent and FF HTML. 197 | return content.replace(/\u00a0/g, ' ').replace(/ /g, ' '); 198 | } 199 | 200 | /** 201 | * Will return the version number if the browser is IE. If not will return -1 202 | * TRY NEVER TO USE THIS AND USE FEATURE DETECTION IF POSSIBLE 203 | * @returns {Number} -1 if false or the version number if true 204 | */ 205 | function _isIE() { 206 | var rv = -1 // Return value assumes failure. 207 | , ua = navigator.userAgent 208 | , re; 209 | if (navigator.appName == 'Microsoft Internet Explorer') { 210 | re = /MSIE ([0-9]{1,}[\.0-9]{0,})/; 211 | if (re.exec(ua) != null) { 212 | rv = parseFloat(RegExp.$1, 10); 213 | } 214 | } 215 | return rv; 216 | } 217 | 218 | /** 219 | * Same as the isIE(), but simply returns a boolean 220 | * THIS IS TERRIBLE AND IS ONLY USED BECAUSE FULLSCREEN IN SAFARI IS BORKED 221 | * If some other engine uses WebKit and has support for fullscreen they 222 | * probably wont get native fullscreen until Safari's fullscreen is fixed 223 | * @returns {Boolean} true if Safari 224 | */ 225 | function _isSafari() { 226 | var n = window.navigator; 227 | return n.userAgent.indexOf('Safari') > -1 && n.userAgent.indexOf('Chrome') == -1; 228 | } 229 | 230 | /** 231 | * Same as the isIE(), but simply returns a boolean 232 | * THIS IS TERRIBLE ONLY USE IF ABSOLUTELY NEEDED 233 | * @returns {Boolean} true if Safari 234 | */ 235 | function _isFirefox() { 236 | var n = window.navigator; 237 | return n.userAgent.indexOf('Firefox') > -1 && n.userAgent.indexOf('Seamonkey') == -1; 238 | } 239 | 240 | /** 241 | * Determines if supplied value is a function 242 | * @param {object} object to determine type 243 | */ 244 | function _isFunction(functionToCheck) { 245 | var getType = {}; 246 | return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; 247 | } 248 | 249 | /** 250 | * Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1 251 | * @param {boolean} [deepMerge=false] If true, will deep merge meaning it will merge sub-objects like {obj:obj2{foo:'bar'}} 252 | * @param {object} first object 253 | * @param {object} second object 254 | * @returnss {object} a new object based on obj1 and obj2 255 | */ 256 | function _mergeObjs() { 257 | // copy reference to target object 258 | var target = arguments[0] || {} 259 | , i = 1 260 | , length = arguments.length 261 | , deep = false 262 | , options 263 | , name 264 | , src 265 | , copy 266 | 267 | // Handle a deep copy situation 268 | if (typeof target === "boolean") { 269 | deep = target; 270 | target = arguments[1] || {}; 271 | // skip the boolean and the target 272 | i = 2; 273 | } 274 | 275 | // Handle case when target is a string or something (possible in deep copy) 276 | if (typeof target !== "object" && !_isFunction(target)) { 277 | target = {}; 278 | } 279 | // extend jQuery itself if only one argument is passed 280 | if (length === i) { 281 | target = this; 282 | --i; 283 | } 284 | 285 | for (; i < length; i++) { 286 | // Only deal with non-null/undefined values 287 | if ((options = arguments[i]) != null) { 288 | // Extend the base object 289 | for (name in options) { 290 | // @NOTE: added hasOwnProperty check 291 | if (options.hasOwnProperty(name)) { 292 | src = target[name]; 293 | copy = options[name]; 294 | // Prevent never-ending loop 295 | if (target === copy) { 296 | continue; 297 | } 298 | // Recurse if we're merging object values 299 | if (deep && copy && typeof copy === "object" && !copy.nodeType) { 300 | target[name] = _mergeObjs(deep, 301 | // Never move original objects, clone them 302 | src || (copy.length != null ? [] : {}) 303 | , copy); 304 | } else if (copy !== undefined) { // Don't bring in undefined values 305 | target[name] = copy; 306 | } 307 | } 308 | } 309 | } 310 | } 311 | 312 | // Return the modified object 313 | return target; 314 | } 315 | 316 | /** 317 | * Initiates the EpicEditor object and sets up offline storage as well 318 | * @class Represents an EpicEditor instance 319 | * @param {object} options An optional customization object 320 | * @returns {object} EpicEditor will be returned 321 | */ 322 | function EpicEditor(options) { 323 | // Default settings will be overwritten/extended by options arg 324 | var self = this 325 | , opts = options || {} 326 | , _defaultFileSchema 327 | , _defaultFile 328 | , defaults = { container: 'epiceditor' 329 | , basePath: '' 330 | , textarea: undefined 331 | , clientSideStorage: true 332 | , localStorageName: 'epiceditor' 333 | , useNativeFullscreen: true 334 | , file: { name: null 335 | , defaultContent: '' 336 | , autoSave: 100 // Set to false for no auto saving 337 | } 338 | , theme: { base: '<%= asset_path("base/epiceditor.css") %>' 339 | , preview: '<%= asset_path("preview/github.css") %>' 340 | , editor: '<%= asset_path("editor/epic-dark.css") %>' 341 | } 342 | , focusOnLoad: false 343 | , shortcut: { modifier: 18 // alt keycode 344 | , fullscreen: 70 // f keycode 345 | , preview: 80 // p keycode 346 | } 347 | , string: { togglePreview: 'Toggle Preview Mode' 348 | , toggleEdit: 'Toggle Edit Mode' 349 | , toggleFullscreen: 'Enter Fullscreen' 350 | } 351 | , parser: typeof marked == 'function' ? marked : null 352 | , autogrow: false 353 | , button: { fullscreen: true 354 | , preview: true 355 | , bar: "auto" 356 | } 357 | } 358 | , defaultStorage 359 | , autogrowDefaults = { minHeight: 80 360 | , maxHeight: false 361 | , scroll: true 362 | }; 363 | 364 | self.settings = _mergeObjs(true, defaults, opts); 365 | 366 | var buttons = self.settings.button; 367 | self._fullscreenEnabled = typeof(buttons) === 'object' ? typeof buttons.fullscreen === 'undefined' || buttons.fullscreen : buttons === true; 368 | self._editEnabled = typeof(buttons) === 'object' ? typeof buttons.edit === 'undefined' || buttons.edit : buttons === true; 369 | self._previewEnabled = typeof(buttons) === 'object' ? typeof buttons.preview === 'undefined' || buttons.preview : buttons === true; 370 | 371 | if (!(typeof self.settings.parser == 'function' && typeof self.settings.parser('TEST') == 'string')) { 372 | self.settings.parser = function (str) { 373 | return str; 374 | } 375 | } 376 | 377 | if (self.settings.autogrow) { 378 | if (self.settings.autogrow === true) { 379 | self.settings.autogrow = autogrowDefaults; 380 | } 381 | else { 382 | self.settings.autogrow = _mergeObjs(true, autogrowDefaults, self.settings.autogrow); 383 | } 384 | self._oldHeight = -1; 385 | } 386 | 387 | // If you put an absolute link as the path of any of the themes ignore the basePath 388 | // preview theme 389 | if (!self.settings.theme.preview.match(/^https?:\/\//)) { 390 | self.settings.theme.preview = self.settings.basePath + self.settings.theme.preview; 391 | } 392 | // editor theme 393 | if (!self.settings.theme.editor.match(/^https?:\/\//)) { 394 | self.settings.theme.editor = self.settings.basePath + self.settings.theme.editor; 395 | } 396 | // base theme 397 | if (!self.settings.theme.base.match(/^https?:\/\//)) { 398 | self.settings.theme.base = self.settings.basePath + self.settings.theme.base; 399 | } 400 | 401 | // Grab the container element and save it to self.element 402 | // if it's a string assume it's an ID and if it's an object 403 | // assume it's a DOM element 404 | if (typeof self.settings.container == 'string') { 405 | self.element = document.getElementById(self.settings.container); 406 | } 407 | else if (typeof self.settings.container == 'object') { 408 | self.element = self.settings.container; 409 | } 410 | 411 | if (typeof self.settings.textarea == 'undefined' && typeof self.element != 'undefined') { 412 | var textareas = self.element.getElementsByTagName('textarea'); 413 | if (textareas.length > 0) { 414 | self.settings.textarea = textareas[0]; 415 | _applyStyles(self.settings.textarea, { 416 | display: 'none' 417 | }); 418 | } 419 | } 420 | 421 | // Figure out the file name. If no file name is given we'll use the ID. 422 | // If there's no ID either we'll use a namespaced file name that's incremented 423 | // based on the calling order. As long as it doesn't change, drafts will be saved. 424 | if (!self.settings.file.name) { 425 | if (typeof self.settings.container == 'string') { 426 | self.settings.file.name = self.settings.container; 427 | } 428 | else if (typeof self.settings.container == 'object') { 429 | if (self.element.id) { 430 | self.settings.file.name = self.element.id; 431 | } 432 | else { 433 | if (!EpicEditor._data.unnamedEditors) { 434 | EpicEditor._data.unnamedEditors = []; 435 | } 436 | EpicEditor._data.unnamedEditors.push(self); 437 | self.settings.file.name = '__epiceditor-untitled-' + EpicEditor._data.unnamedEditors.length; 438 | } 439 | } 440 | } 441 | 442 | if (self.settings.button.bar === "show") { 443 | self.settings.button.bar = true; 444 | } 445 | 446 | if (self.settings.button.bar === "hide") { 447 | self.settings.button.bar = false; 448 | } 449 | 450 | // Protect the id and overwrite if passed in as an option 451 | // TODO: Put underscrore to denote that this is private 452 | self._instanceId = 'epiceditor-' + Math.round(Math.random() * 100000); 453 | self._storage = {}; 454 | self._canSave = true; 455 | 456 | // Setup local storage of files 457 | self._defaultFileSchema = function () { 458 | return { 459 | content: self.settings.file.defaultContent 460 | , created: new Date() 461 | , modified: new Date() 462 | } 463 | } 464 | 465 | if (localStorage && self.settings.clientSideStorage) { 466 | this._storage = localStorage; 467 | if (this._storage[self.settings.localStorageName] && self.getFiles(self.settings.file.name) === undefined) { 468 | _defaultFile = self._defaultFileSchema(); 469 | _defaultFile.content = self.settings.file.defaultContent; 470 | } 471 | } 472 | 473 | if (!this._storage[self.settings.localStorageName]) { 474 | defaultStorage = {}; 475 | defaultStorage[self.settings.file.name] = self._defaultFileSchema(); 476 | defaultStorage = JSON.stringify(defaultStorage); 477 | this._storage[self.settings.localStorageName] = defaultStorage; 478 | } 479 | 480 | // A string to prepend files with to save draft versions of files 481 | // and reset all preview drafts on each load! 482 | self._previewDraftLocation = '__draft-'; 483 | self._storage[self._previewDraftLocation + self.settings.localStorageName] = self._storage[self.settings.localStorageName]; 484 | 485 | // This needs to replace the use of classes to check the state of EE 486 | self._eeState = { 487 | fullscreen: false 488 | , preview: false 489 | , edit: false 490 | , loaded: false 491 | , unloaded: false 492 | } 493 | 494 | // Now that it exists, allow binding of events if it doesn't exist yet 495 | if (!self.events) { 496 | self.events = {}; 497 | } 498 | 499 | return this; 500 | } 501 | 502 | /** 503 | * Inserts the EpicEditor into the DOM via an iframe and gets it ready for editing and previewing 504 | * @returns {object} EpicEditor will be returned 505 | */ 506 | EpicEditor.prototype.load = function (callback) { 507 | 508 | // Get out early if it's already loaded 509 | if (this.is('loaded')) { return this; } 510 | 511 | // TODO: Gotta get the privates with underscores! 512 | // TODO: Gotta document what these are for... 513 | var self = this 514 | , _HtmlTemplates 515 | , iframeElement 516 | , baseTag 517 | , utilBtns 518 | , utilBar 519 | , utilBarTimer 520 | , keypressTimer 521 | , mousePos = { y: -1, x: -1 } 522 | , _elementStates 523 | , _isInEdit 524 | , nativeFs = false 525 | , nativeFsWebkit = false 526 | , nativeFsMoz = false 527 | , nativeFsW3C = false 528 | , fsElement 529 | , isMod = false 530 | , isCtrl = false 531 | , eventableIframes 532 | , i // i is reused for loops 533 | , boundAutogrow; 534 | 535 | // Startup is a way to check if this EpicEditor is starting up. Useful for 536 | // checking and doing certain things before EpicEditor emits a load event. 537 | self._eeState.startup = true; 538 | 539 | if (self.settings.useNativeFullscreen) { 540 | nativeFsWebkit = document.body.webkitRequestFullScreen ? true : false; 541 | nativeFsMoz = document.body.mozRequestFullScreen ? true : false; 542 | nativeFsW3C = document.body.requestFullscreen ? true : false; 543 | nativeFs = nativeFsWebkit || nativeFsMoz || nativeFsW3C; 544 | } 545 | 546 | // Fucking Safari's native fullscreen works terribly 547 | // REMOVE THIS IF SAFARI 7 WORKS BETTER 548 | if (_isSafari()) { 549 | nativeFs = false; 550 | nativeFsWebkit = false; 551 | } 552 | 553 | // It opens edit mode by default (for now); 554 | if (!self.is('edit') && !self.is('preview')) { 555 | self._eeState.edit = true; 556 | } 557 | 558 | callback = callback || function () {}; 559 | 560 | // The editor HTML 561 | // TODO: edit-mode class should be dynamically added 562 | _HtmlTemplates = { 563 | // This is wrapping iframe element. It contains the other two iframes and the utilbar 564 | chrome: '
' + 565 | '' + 566 | '' + 567 | '
' + 568 | (self._previewEnabled ? ' ' : '') + 569 | (self._editEnabled ? ' ' : '') + 570 | (self._fullscreenEnabled ? '' : '') + 571 | '
' + 572 | '
' 573 | 574 | // The previewer is just an empty box for the generated HTML to go into 575 | , previewer: '
' 576 | , editor: '' 577 | }; 578 | 579 | // Write an iframe and then select it for the editor 580 | iframeElement = document.createElement('iframe'); 581 | _applyAttrs(iframeElement, { 582 | scrolling: 'no', 583 | frameborder: 0, 584 | id: self._instanceId 585 | }); 586 | 587 | 588 | self.element.appendChild(iframeElement); 589 | 590 | // Because browsers add things like invisible padding and margins and stuff 591 | // to iframes, we need to set manually set the height so that the height 592 | // doesn't keep increasing (by 2px?) every time reflow() is called. 593 | // FIXME: Figure out how to fix this without setting this 594 | self.element.style.height = self.element.offsetHeight + 'px'; 595 | 596 | // Store a reference to the iframeElement itself 597 | self.iframeElement = iframeElement; 598 | 599 | // Grab the innards of the iframe (returns the document.body) 600 | // TODO: Change self.iframe to self.iframeDocument 601 | self.iframe = _getIframeInnards(iframeElement); 602 | self.iframe.open(); 603 | self.iframe.write(_HtmlTemplates.chrome); 604 | 605 | // Now that we got the innards of the iframe, we can grab the other iframes 606 | self.editorIframe = self.iframe.getElementById('epiceditor-editor-frame') 607 | self.previewerIframe = self.iframe.getElementById('epiceditor-previewer-frame'); 608 | 609 | // Setup the editor iframe 610 | self.editorIframeDocument = _getIframeInnards(self.editorIframe); 611 | self.editorIframeDocument.open(); 612 | // Need something for... you guessed it, Firefox 613 | self.editorIframeDocument.write(_HtmlTemplates.editor); 614 | self.editorIframeDocument.close(); 615 | 616 | // Setup the previewer iframe 617 | self.previewerIframeDocument = _getIframeInnards(self.previewerIframe); 618 | self.previewerIframeDocument.open(); 619 | self.previewerIframeDocument.write(_HtmlTemplates.previewer); 620 | 621 | // Base tag is added so that links will open a new tab and not inside of the iframes 622 | baseTag = self.previewerIframeDocument.createElement('base'); 623 | baseTag.target = '_blank'; 624 | self.previewerIframeDocument.getElementsByTagName('head')[0].appendChild(baseTag); 625 | 626 | self.previewerIframeDocument.close(); 627 | 628 | self.reflow(); 629 | 630 | // Insert Base Stylesheet 631 | _insertCSSLink(self.settings.theme.base, self.iframe, 'theme'); 632 | 633 | // Insert Editor Stylesheet 634 | _insertCSSLink(self.settings.theme.editor, self.editorIframeDocument, 'theme'); 635 | 636 | // Insert Previewer Stylesheet 637 | _insertCSSLink(self.settings.theme.preview, self.previewerIframeDocument, 'theme'); 638 | 639 | // Add a relative style to the overall wrapper to keep CSS relative to the editor 640 | self.iframe.getElementById('epiceditor-wrapper').style.position = 'relative'; 641 | 642 | // Set the position to relative so we hide them with left: -999999px 643 | self.editorIframe.style.position = 'absolute'; 644 | self.previewerIframe.style.position = 'absolute'; 645 | 646 | // Now grab the editor and previewer for later use 647 | self.editor = self.editorIframeDocument.body; 648 | self.previewer = self.previewerIframeDocument.getElementById('epiceditor-preview'); 649 | 650 | self.editor.contentEditable = true; 651 | 652 | // Firefox's gets all fucked up so, to be sure, we need to hardcode it 653 | self.iframe.body.style.height = this.element.offsetHeight + 'px'; 654 | 655 | // Should actually check what mode it's in! 656 | self.previewerIframe.style.left = '-999999px'; 657 | 658 | // Keep long lines from being longer than the editor 659 | this.editorIframeDocument.body.style.wordWrap = 'break-word'; 660 | 661 | // FIXME figure out why it needs +2 px 662 | if (_isIE() > -1) { 663 | this.previewer.style.height = parseInt(_getStyle(this.previewer, 'height'), 10) + 2; 664 | } 665 | 666 | // If there is a file to be opened with that filename and it has content... 667 | this.open(self.settings.file.name); 668 | 669 | if (self.settings.focusOnLoad) { 670 | // We need to wait until all three iframes are done loading by waiting until the parent 671 | // iframe's ready state == complete, then we can focus on the contenteditable 672 | self.iframe.addEventListener('readystatechange', function () { 673 | if (self.iframe.readyState == 'complete') { 674 | self.focus(); 675 | } 676 | }); 677 | } 678 | 679 | // Because IE scrolls the whole window to hash links, we need our own 680 | // method of scrolling the iframe to an ID from clicking a hash 681 | self.previewerIframeDocument.addEventListener('click', function (e) { 682 | var el = e.target 683 | , body = self.previewerIframeDocument.body; 684 | if (el.nodeName == 'A') { 685 | // Make sure the link is a hash and the link is local to the iframe 686 | if (el.hash && el.hostname == window.location.hostname) { 687 | // Prevent the whole window from scrolling 688 | e.preventDefault(); 689 | // Prevent opening a new window 690 | el.target = '_self'; 691 | // Scroll to the matching element, if an element exists 692 | if (body.querySelector(el.hash)) { 693 | body.scrollTop = body.querySelector(el.hash).offsetTop; 694 | } 695 | } 696 | } 697 | }); 698 | 699 | utilBtns = self.iframe.getElementById('epiceditor-utilbar'); 700 | 701 | // TODO: Move into fullscreen setup function (_setupFullscreen) 702 | _elementStates = {} 703 | self._goFullscreen = function (el, callback) { 704 | callback = callback || function () {}; 705 | var wait = 0; 706 | this._fixScrollbars('auto'); 707 | 708 | if (self.is('fullscreen')) { 709 | self._exitFullscreen(el, callback); 710 | return; 711 | } 712 | 713 | if (nativeFs) { 714 | if (nativeFsWebkit) { 715 | el.webkitRequestFullScreen(); 716 | wait = 750; 717 | } 718 | else if (nativeFsMoz) { 719 | el.mozRequestFullScreen(); 720 | } 721 | else if (nativeFsW3C) { 722 | el.requestFullscreen(); 723 | } 724 | } 725 | 726 | _isInEdit = self.is('edit'); 727 | 728 | 729 | // Why does this need to be in a randomly "750"ms setTimeout? WebKit's 730 | // implementation of fullscreen seem to trigger the webkitfullscreenchange 731 | // event _after_ everything is done. Instead, it triggers _during_ the 732 | // transition. This means calculations of what's half, 100%, etc are wrong 733 | // so to combat this we throw down the hammer with a setTimeout and wait 734 | // to trigger our calculation code. 735 | // See: https://code.google.com/p/chromium/issues/detail?id=181116 736 | setTimeout(function () { 737 | // Set the state of EE in fullscreen 738 | // We set edit and preview to true also because they're visible 739 | // we might want to allow fullscreen edit mode without preview (like a "zen" mode) 740 | self._eeState.fullscreen = true; 741 | self._eeState.edit = true; 742 | self._eeState.preview = true; 743 | 744 | // Cache calculations 745 | var windowInnerWidth = window.innerWidth 746 | , windowInnerHeight = window.innerHeight 747 | , windowOuterWidth = window.outerWidth 748 | , windowOuterHeight = window.outerHeight; 749 | 750 | // Without this the scrollbars will get hidden when scrolled to the bottom in faux fullscreen (see #66) 751 | if (!nativeFs) { 752 | windowOuterHeight = window.innerHeight; 753 | } 754 | 755 | // This MUST come first because the editor is 100% width so if we change the width of the iframe or wrapper 756 | // the editor's width wont be the same as before 757 | _elementStates.editorIframe = _saveStyleState(self.editorIframe, 'save', { 758 | 'width': windowOuterWidth / 2 + 'px' 759 | , 'height': windowOuterHeight + 'px' 760 | , 'float': 'left' // Most browsers 761 | , 'cssFloat': 'left' // FF 762 | , 'styleFloat': 'left' // Older IEs 763 | , 'display': 'block' 764 | , 'position': 'static' 765 | , 'left': '' 766 | }); 767 | 768 | // the previewer 769 | _elementStates.previewerIframe = _saveStyleState(self.previewerIframe, 'save', { 770 | 'width': windowOuterWidth / 2 + 'px' 771 | , 'height': windowOuterHeight + 'px' 772 | , 'float': 'right' // Most browsers 773 | , 'cssFloat': 'right' // FF 774 | , 'styleFloat': 'right' // Older IEs 775 | , 'display': 'block' 776 | , 'position': 'static' 777 | , 'left': '' 778 | }); 779 | 780 | // Setup the containing element CSS for fullscreen 781 | _elementStates.element = _saveStyleState(self.element, 'save', { 782 | 'position': 'fixed' 783 | , 'top': '0' 784 | , 'left': '0' 785 | , 'width': '100%' 786 | , 'z-index': '9999' // Most browsers 787 | , 'zIndex': '9999' // Firefox 788 | , 'border': 'none' 789 | , 'margin': '0' 790 | // Should use the base styles background! 791 | , 'background': _getStyle(self.editor, 'background-color') // Try to hide the site below 792 | , 'height': windowInnerHeight + 'px' 793 | }); 794 | 795 | // The iframe element 796 | _elementStates.iframeElement = _saveStyleState(self.iframeElement, 'save', { 797 | 'width': windowOuterWidth + 'px' 798 | , 'height': windowInnerHeight + 'px' 799 | }); 800 | 801 | // ...Oh, and hide the buttons and prevent scrolling 802 | utilBtns.style.visibility = 'hidden'; 803 | 804 | if (!nativeFs) { 805 | document.body.style.overflow = 'hidden'; 806 | } 807 | 808 | self.preview(); 809 | 810 | self.focus(); 811 | 812 | self.emit('fullscreenenter'); 813 | 814 | callback.call(self); 815 | }, wait); 816 | 817 | }; 818 | 819 | self._exitFullscreen = function (el, callback) { 820 | callback = callback || function () {}; 821 | this._fixScrollbars(); 822 | 823 | _saveStyleState(self.element, 'apply', _elementStates.element); 824 | _saveStyleState(self.iframeElement, 'apply', _elementStates.iframeElement); 825 | _saveStyleState(self.editorIframe, 'apply', _elementStates.editorIframe); 826 | _saveStyleState(self.previewerIframe, 'apply', _elementStates.previewerIframe); 827 | 828 | // We want to always revert back to the original styles in the CSS so, 829 | // if it's a fluid width container it will expand on resize and not get 830 | // stuck at a specific width after closing fullscreen. 831 | self.element.style.width = self._eeState.reflowWidth ? self._eeState.reflowWidth : ''; 832 | self.element.style.height = self._eeState.reflowHeight ? self._eeState.reflowHeight : ''; 833 | 834 | utilBtns.style.visibility = 'visible'; 835 | 836 | // Put the editor back in the right state 837 | // TODO: This is ugly... how do we make this nicer? 838 | // setting fullscreen to false here prevents the 839 | // native fs callback from calling this function again 840 | self._eeState.fullscreen = false; 841 | 842 | if (!nativeFs) { 843 | document.body.style.overflow = 'auto'; 844 | } 845 | else { 846 | if (nativeFsWebkit) { 847 | document.webkitCancelFullScreen(); 848 | } 849 | else if (nativeFsMoz) { 850 | document.mozCancelFullScreen(); 851 | } 852 | else if (nativeFsW3C) { 853 | document.exitFullscreen(); 854 | } 855 | } 856 | 857 | if (_isInEdit) { 858 | self.edit(); 859 | } 860 | else { 861 | self.preview(); 862 | } 863 | 864 | self.reflow(); 865 | 866 | self.emit('fullscreenexit'); 867 | 868 | callback.call(self); 869 | }; 870 | 871 | // This setups up live previews by triggering preview() IF in fullscreen on keyup 872 | self.editor.addEventListener('keyup', function () { 873 | if (keypressTimer) { 874 | window.clearTimeout(keypressTimer); 875 | } 876 | keypressTimer = window.setTimeout(function () { 877 | if (self.is('fullscreen')) { 878 | self.preview(); 879 | } 880 | }, 250); 881 | }); 882 | 883 | fsElement = self.iframeElement; 884 | 885 | // Sets up the onclick event on utility buttons 886 | utilBtns.addEventListener('click', function (e) { 887 | var targetClass = e.target.className; 888 | if (targetClass.indexOf('epiceditor-toggle-preview-btn') > -1) { 889 | self.preview(); 890 | } 891 | else if (targetClass.indexOf('epiceditor-toggle-edit-btn') > -1) { 892 | self.edit(); 893 | } 894 | else if (targetClass.indexOf('epiceditor-fullscreen-btn') > -1) { 895 | self._goFullscreen(fsElement); 896 | } 897 | }); 898 | 899 | // Sets up the NATIVE fullscreen editor/previewer for WebKit 900 | if (nativeFsWebkit) { 901 | document.addEventListener('webkitfullscreenchange', function () { 902 | if (!document.webkitIsFullScreen && self._eeState.fullscreen) { 903 | self._exitFullscreen(fsElement); 904 | } 905 | }, false); 906 | } 907 | else if (nativeFsMoz) { 908 | document.addEventListener('mozfullscreenchange', function () { 909 | if (!document.mozFullScreen && self._eeState.fullscreen) { 910 | self._exitFullscreen(fsElement); 911 | } 912 | }, false); 913 | } 914 | else if (nativeFsW3C) { 915 | document.addEventListener('fullscreenchange', function () { 916 | if (document.fullscreenElement == null && self._eeState.fullscreen) { 917 | self._exitFullscreen(fsElement); 918 | } 919 | }, false); 920 | } 921 | 922 | // TODO: Move utilBar stuff into a utilBar setup function (_setupUtilBar) 923 | utilBar = self.iframe.getElementById('epiceditor-utilbar'); 924 | 925 | // Hide it at first until they move their mouse 926 | if (self.settings.button.bar !== true) { 927 | utilBar.style.display = 'none'; 928 | } 929 | 930 | utilBar.addEventListener('mouseover', function () { 931 | if (utilBarTimer) { 932 | clearTimeout(utilBarTimer); 933 | } 934 | }); 935 | 936 | function utilBarHandler(e) { 937 | if (self.settings.button.bar !== "auto") { 938 | return; 939 | } 940 | // Here we check if the mouse has moves more than 5px in any direction before triggering the mousemove code 941 | // we do this for 2 reasons: 942 | // 1. On Mac OS X lion when you scroll and it does the iOS like "jump" when it hits the top/bottom of the page itll fire off 943 | // a mousemove of a few pixels depending on how hard you scroll 944 | // 2. We give a slight buffer to the user in case he barely touches his touchpad or mouse and not trigger the UI 945 | if (Math.abs(mousePos.y - e.pageY) >= 5 || Math.abs(mousePos.x - e.pageX) >= 5) { 946 | utilBar.style.display = 'block'; 947 | // if we have a timer already running, kill it out 948 | if (utilBarTimer) { 949 | clearTimeout(utilBarTimer); 950 | } 951 | 952 | // begin a new timer that hides our object after 1000 ms 953 | utilBarTimer = window.setTimeout(function () { 954 | utilBar.style.display = 'none'; 955 | }, 1000); 956 | } 957 | mousePos = { y: e.pageY, x: e.pageX }; 958 | } 959 | 960 | // Add keyboard shortcuts for convenience. 961 | function shortcutHandler(e) { 962 | if (e.keyCode == self.settings.shortcut.modifier) { isMod = true } // check for modifier press(default is alt key), save to var 963 | if (e.keyCode == 17) { isCtrl = true } // check for ctrl/cmnd press, in order to catch ctrl/cmnd + s 964 | if (e.keyCode == 18) { isCtrl = false } 965 | 966 | // Check for alt+p and make sure were not in fullscreen - default shortcut to switch to preview 967 | if (isMod === true && e.keyCode == self.settings.shortcut.preview && !self.is('fullscreen')) { 968 | e.preventDefault(); 969 | if (self.is('edit') && self._previewEnabled) { 970 | self.preview(); 971 | } 972 | else if (self._editEnabled) { 973 | self.edit(); 974 | } 975 | } 976 | // Check for alt+f - default shortcut to make editor fullscreen 977 | if (isMod === true && e.keyCode == self.settings.shortcut.fullscreen && self._fullscreenEnabled) { 978 | e.preventDefault(); 979 | self._goFullscreen(fsElement); 980 | } 981 | 982 | // Set the modifier key to false once *any* key combo is completed 983 | // or else, on Windows, hitting the alt key will lock the isMod state to true (ticket #133) 984 | if (isMod === true && e.keyCode !== self.settings.shortcut.modifier) { 985 | isMod = false; 986 | } 987 | 988 | // When a user presses "esc", revert everything! 989 | if (e.keyCode == 27 && self.is('fullscreen')) { 990 | self._exitFullscreen(fsElement); 991 | } 992 | 993 | // Check for ctrl + s (since a lot of people do it out of habit) and make it do nothing 994 | if (isCtrl === true && e.keyCode == 83) { 995 | self.save(); 996 | e.preventDefault(); 997 | isCtrl = false; 998 | } 999 | 1000 | // Do the same for Mac now (metaKey == cmd). 1001 | if (e.metaKey && e.keyCode == 83) { 1002 | self.save(); 1003 | e.preventDefault(); 1004 | } 1005 | 1006 | } 1007 | 1008 | function shortcutUpHandler(e) { 1009 | if (e.keyCode == self.settings.shortcut.modifier) { isMod = false } 1010 | if (e.keyCode == 17) { isCtrl = false } 1011 | } 1012 | 1013 | function pasteHandler(e) { 1014 | var content; 1015 | if (e.clipboardData) { 1016 | //FF 22, Webkit, "standards" 1017 | e.preventDefault(); 1018 | content = e.clipboardData.getData("text/plain"); 1019 | self.editorIframeDocument.execCommand("insertText", false, content); 1020 | } 1021 | else if (window.clipboardData) { 1022 | //IE, "nasty" 1023 | e.preventDefault(); 1024 | content = window.clipboardData.getData("Text"); 1025 | content = content.replace(//g, '>'); 1027 | content = content.replace(/\n/g, '
'); 1028 | content = content.replace(/\r/g, ''); //fuck you, ie! 1029 | content = content.replace(/
\s/g, '
 ') 1030 | content = content.replace(/\s\s\s/g, '   ') 1031 | content = content.replace(/\s\s/g, '  ') 1032 | self.editorIframeDocument.selection.createRange().pasteHTML(content); 1033 | } 1034 | } 1035 | 1036 | // Hide and show the util bar based on mouse movements 1037 | eventableIframes = [self.previewerIframeDocument, self.editorIframeDocument]; 1038 | 1039 | for (i = 0; i < eventableIframes.length; i++) { 1040 | eventableIframes[i].addEventListener('mousemove', function (e) { 1041 | utilBarHandler(e); 1042 | }); 1043 | eventableIframes[i].addEventListener('scroll', function (e) { 1044 | utilBarHandler(e); 1045 | }); 1046 | eventableIframes[i].addEventListener('keyup', function (e) { 1047 | shortcutUpHandler(e); 1048 | }); 1049 | eventableIframes[i].addEventListener('keydown', function (e) { 1050 | shortcutHandler(e); 1051 | }); 1052 | eventableIframes[i].addEventListener('paste', function (e) { 1053 | pasteHandler(e); 1054 | }); 1055 | } 1056 | 1057 | // Save the document every 100ms by default 1058 | // TODO: Move into autosave setup function (_setupAutoSave) 1059 | if (self.settings.file.autoSave) { 1060 | self._saveIntervalTimer = window.setInterval(function () { 1061 | if (!self._canSave) { 1062 | return; 1063 | } 1064 | self.save(false, true); 1065 | }, self.settings.file.autoSave); 1066 | } 1067 | 1068 | // Update a textarea automatically if a textarea is given so you don't need 1069 | // AJAX to submit a form and instead fall back to normal form behavior 1070 | if (self.settings.textarea) { 1071 | self._setupTextareaSync(); 1072 | } 1073 | 1074 | window.addEventListener('resize', function () { 1075 | // If NOT webkit, and in fullscreen, we need to account for browser resizing 1076 | // we don't care about webkit because you can't resize in webkit's fullscreen 1077 | if (self.is('fullscreen')) { 1078 | _applyStyles(self.iframeElement, { 1079 | 'width': window.outerWidth + 'px' 1080 | , 'height': window.innerHeight + 'px' 1081 | }); 1082 | 1083 | _applyStyles(self.element, { 1084 | 'height': window.innerHeight + 'px' 1085 | }); 1086 | 1087 | _applyStyles(self.previewerIframe, { 1088 | 'width': window.outerWidth / 2 + 'px' 1089 | , 'height': window.innerHeight + 'px' 1090 | }); 1091 | 1092 | _applyStyles(self.editorIframe, { 1093 | 'width': window.outerWidth / 2 + 'px' 1094 | , 'height': window.innerHeight + 'px' 1095 | }); 1096 | } 1097 | // Makes the editor support fluid width when not in fullscreen mode 1098 | else if (!self.is('fullscreen')) { 1099 | self.reflow(); 1100 | } 1101 | }); 1102 | 1103 | // Set states before flipping edit and preview modes 1104 | self._eeState.loaded = true; 1105 | self._eeState.unloaded = false; 1106 | 1107 | if (self.is('preview')) { 1108 | self.preview(); 1109 | } 1110 | else { 1111 | self.edit(); 1112 | } 1113 | 1114 | self.iframe.close(); 1115 | self._eeState.startup = false; 1116 | 1117 | if (self.settings.autogrow) { 1118 | self._fixScrollbars(); 1119 | 1120 | boundAutogrow = function () { 1121 | setTimeout(function () { 1122 | self._autogrow(); 1123 | }, 1); 1124 | }; 1125 | 1126 | //for if autosave is disabled or very slow 1127 | ['keydown', 'keyup', 'paste', 'cut'].forEach(function (ev) { 1128 | self.getElement('editor').addEventListener(ev, boundAutogrow); 1129 | }); 1130 | 1131 | self.on('__update', boundAutogrow); 1132 | self.on('edit', function () { 1133 | setTimeout(boundAutogrow, 50) 1134 | }); 1135 | self.on('preview', function () { 1136 | setTimeout(boundAutogrow, 50) 1137 | }); 1138 | 1139 | //for browsers that have rendering delays 1140 | setTimeout(boundAutogrow, 50); 1141 | boundAutogrow(); 1142 | } 1143 | 1144 | // The callback and call are the same thing, but different ways to access them 1145 | callback.call(this); 1146 | this.emit('load'); 1147 | return this; 1148 | } 1149 | 1150 | EpicEditor.prototype._setupTextareaSync = function () { 1151 | var self = this 1152 | , _syncTextarea; 1153 | 1154 | // Even if autoSave is false, we want to make sure to keep the textarea synced 1155 | // with the editor's content. One bad thing about this tho is that we're 1156 | // creating two timers now in some configurations. We keep the textarea synced 1157 | // by saving and opening the textarea content from the draft file storage. 1158 | self._textareaSaveTimer = window.setInterval(function () { 1159 | if (!self._canSave) { 1160 | return; 1161 | } 1162 | self.save(true); 1163 | }, 100); 1164 | 1165 | _syncTextarea = function () { 1166 | // TODO: Figure out root cause for having to do this ||. 1167 | // This only happens for draft files. Probably has something to do with 1168 | // the fact draft files haven't been saved by the time this is called. 1169 | // TODO: Add test for this case. 1170 | // Get the file.name each time as it can change. DO NOT save this to a 1171 | // var outside of this closure or the editor will stop syncing when the 1172 | // file is changed with importFile or open. 1173 | self._textareaElement.value = self.exportFile(self.settings.file.name, 'text', true) || self.settings.file.defaultContent; 1174 | } 1175 | 1176 | if (typeof self.settings.textarea == 'string') { 1177 | self._textareaElement = document.getElementById(self.settings.textarea); 1178 | } 1179 | else if (typeof self.settings.textarea == 'object') { 1180 | self._textareaElement = self.settings.textarea; 1181 | } 1182 | 1183 | // On page load, if there's content in the textarea that means one of two 1184 | // different things: 1185 | // 1186 | // 1. The editor didn't load and the user was writing in the textarea and 1187 | // now he refreshed the page or the JS loaded and the textarea now has 1188 | // content. If this is the case the user probably expects his content is 1189 | // moved into the editor and not lose what he typed. 1190 | // 1191 | // 2. The developer put content in the textarea from some server side 1192 | // code. In this case, the textarea will take precedence. 1193 | // 1194 | // If the developer wants drafts to be recoverable they should check if 1195 | // the local file in localStorage's modified date is newer than the server. 1196 | if (self._textareaElement.value !== '') { 1197 | self.importFile(self.settings.file.name, self._textareaElement.value); 1198 | 1199 | // manually save draft after import so there is no delay between the 1200 | // import and exporting in _syncTextarea. Without this, _syncTextarea 1201 | // will pull the saved data from localStorage which will be <=100ms old. 1202 | self.save(true); 1203 | } 1204 | 1205 | // Update the textarea on load and pull from drafts 1206 | _syncTextarea(); 1207 | 1208 | // Make sure to keep it updated 1209 | self.on('__update', _syncTextarea); 1210 | self.on('__create', _syncTextarea); 1211 | self.on('__save', _syncTextarea); 1212 | } 1213 | 1214 | /** 1215 | * Will NOT focus the editor if the editor is still starting up AND 1216 | * focusOnLoad is set to false. This allows you to place this in code that 1217 | * gets fired during .load() without worrying about it overriding the user's 1218 | * option. For example use cases see preview() and edit(). 1219 | * @returns {undefined} 1220 | */ 1221 | 1222 | // Prevent focus when the user sets focusOnLoad to false by checking if the 1223 | // editor is starting up AND if focusOnLoad is true 1224 | EpicEditor.prototype._focusExceptOnLoad = function () { 1225 | var self = this; 1226 | if ((self._eeState.startup && self.settings.focusOnLoad) || !self._eeState.startup) { 1227 | self.focus(); 1228 | } 1229 | } 1230 | 1231 | /** 1232 | * Will remove the editor, but not offline files 1233 | * @returns {object} EpicEditor will be returned 1234 | */ 1235 | EpicEditor.prototype.unload = function (callback) { 1236 | 1237 | // Make sure the editor isn't already unloaded. 1238 | if (this.is('unloaded')) { 1239 | throw new Error('Editor isn\'t loaded'); 1240 | } 1241 | 1242 | var self = this 1243 | , editor = window.parent.document.getElementById(self._instanceId); 1244 | 1245 | editor.parentNode.removeChild(editor); 1246 | self._eeState.loaded = false; 1247 | self._eeState.unloaded = true; 1248 | callback = callback || function () {}; 1249 | 1250 | if (self.settings.textarea) { 1251 | self.removeListener('__update'); 1252 | } 1253 | 1254 | if (self._saveIntervalTimer) { 1255 | window.clearInterval(self._saveIntervalTimer); 1256 | } 1257 | if (self._textareaSaveTimer) { 1258 | window.clearInterval(self._textareaSaveTimer); 1259 | } 1260 | 1261 | callback.call(this); 1262 | self.emit('unload'); 1263 | return self; 1264 | } 1265 | 1266 | /** 1267 | * reflow allows you to dynamically re-fit the editor in the parent without 1268 | * having to unload and then reload the editor again. 1269 | * 1270 | * reflow will also emit a `reflow` event and will return the new dimensions. 1271 | * If it's called without params it'll return the new width and height and if 1272 | * it's called with just width or just height it'll just return the width or 1273 | * height. It's returned as an object like: { width: '100px', height: '1px' } 1274 | * 1275 | * @param {string|null} kind Can either be 'width' or 'height' or null 1276 | * if null, both the height and width will be resized 1277 | * @param {function} callback A function to fire after the reflow is finished. 1278 | * Will return the width / height in an obj as the first param of the callback. 1279 | * @returns {object} EpicEditor will be returned 1280 | */ 1281 | EpicEditor.prototype.reflow = function (kind, callback) { 1282 | var self = this 1283 | , widthDiff = _outerWidth(self.element) - self.element.offsetWidth 1284 | , heightDiff = _outerHeight(self.element) - self.element.offsetHeight 1285 | , elements = [self.iframeElement, self.editorIframe, self.previewerIframe] 1286 | , eventData = {} 1287 | , newWidth 1288 | , newHeight; 1289 | 1290 | if (typeof kind == 'function') { 1291 | callback = kind; 1292 | kind = null; 1293 | } 1294 | 1295 | if (!callback) { 1296 | callback = function () {}; 1297 | } 1298 | 1299 | for (var x = 0; x < elements.length; x++) { 1300 | if (!kind || kind == 'width') { 1301 | newWidth = self.element.offsetWidth - widthDiff + 'px'; 1302 | elements[x].style.width = newWidth; 1303 | self._eeState.reflowWidth = newWidth; 1304 | eventData.width = newWidth; 1305 | } 1306 | if (!kind || kind == 'height') { 1307 | newHeight = self.element.offsetHeight - heightDiff + 'px'; 1308 | elements[x].style.height = newHeight; 1309 | self._eeState.reflowHeight = newHeight 1310 | eventData.height = newHeight; 1311 | } 1312 | } 1313 | 1314 | self.emit('reflow', eventData); 1315 | callback.call(this, eventData); 1316 | return self; 1317 | } 1318 | 1319 | /** 1320 | * Will take the markdown and generate a preview view based on the theme 1321 | * @returns {object} EpicEditor will be returned 1322 | */ 1323 | EpicEditor.prototype.preview = function () { 1324 | var self = this 1325 | , x 1326 | , theme = self.settings.theme.preview 1327 | , anchors; 1328 | 1329 | _replaceClass(self.getElement('wrapper'), 'epiceditor-edit-mode', 'epiceditor-preview-mode'); 1330 | 1331 | // Check if no CSS theme link exists 1332 | if (!self.previewerIframeDocument.getElementById('theme')) { 1333 | _insertCSSLink(theme, self.previewerIframeDocument, 'theme'); 1334 | } 1335 | else if (self.previewerIframeDocument.getElementById('theme').name !== theme) { 1336 | self.previewerIframeDocument.getElementById('theme').href = theme; 1337 | } 1338 | 1339 | // Save a preview draft since it might not be saved to the real file yet 1340 | self.save(true); 1341 | 1342 | // Add the generated draft HTML into the previewer 1343 | self.previewer.innerHTML = self.exportFile(null, 'html', true); 1344 | 1345 | // Hide the editor and display the previewer 1346 | if (!self.is('fullscreen')) { 1347 | self.editorIframe.style.left = '-999999px'; 1348 | self.previewerIframe.style.left = ''; 1349 | self._eeState.preview = true; 1350 | self._eeState.edit = false; 1351 | self._focusExceptOnLoad(); 1352 | } 1353 | 1354 | self.emit('preview'); 1355 | return self; 1356 | } 1357 | 1358 | /** 1359 | * Helper to focus on the editor iframe. Will figure out which iframe to 1360 | * focus on based on which one is active and will handle the cross browser 1361 | * issues with focusing on the iframe vs the document body. 1362 | * @returns {object} EpicEditor will be returned 1363 | */ 1364 | EpicEditor.prototype.focus = function (pageload) { 1365 | var self = this 1366 | , isPreview = self.is('preview') 1367 | , focusElement = isPreview ? self.previewerIframeDocument.body 1368 | : self.editorIframeDocument.body; 1369 | 1370 | if (_isFirefox() && isPreview) { 1371 | focusElement = self.previewerIframe; 1372 | } 1373 | 1374 | focusElement.focus(); 1375 | return this; 1376 | } 1377 | 1378 | /** 1379 | * Puts the editor into fullscreen mode 1380 | * @returns {object} EpicEditor will be returned 1381 | */ 1382 | EpicEditor.prototype.enterFullscreen = function (callback) { 1383 | callback = callback || function () {}; 1384 | if (this.is('fullscreen')) { 1385 | callback.call(this); 1386 | return this; 1387 | } 1388 | this._goFullscreen(this.iframeElement, callback); 1389 | return this; 1390 | } 1391 | 1392 | /** 1393 | * Closes fullscreen mode if opened 1394 | * @returns {object} EpicEditor will be returned 1395 | */ 1396 | EpicEditor.prototype.exitFullscreen = function (callback) { 1397 | callback = callback || function () {}; 1398 | if (!this.is('fullscreen')) { 1399 | callback.call(this); 1400 | return this; 1401 | } 1402 | this._exitFullscreen(this.iframeElement, callback); 1403 | return this; 1404 | } 1405 | 1406 | /** 1407 | * Hides the preview and shows the editor again 1408 | * @returns {object} EpicEditor will be returned 1409 | */ 1410 | EpicEditor.prototype.edit = function () { 1411 | var self = this; 1412 | _replaceClass(self.getElement('wrapper'), 'epiceditor-preview-mode', 'epiceditor-edit-mode'); 1413 | self._eeState.preview = false; 1414 | self._eeState.edit = true; 1415 | self.editorIframe.style.left = ''; 1416 | self.previewerIframe.style.left = '-999999px'; 1417 | self._focusExceptOnLoad(); 1418 | self.emit('edit'); 1419 | return this; 1420 | } 1421 | 1422 | /** 1423 | * Grabs a specificed HTML node. Use it as a shortcut to getting the iframe contents 1424 | * @param {String} name The name of the node (can be document, body, editor, previewer, or wrapper) 1425 | * @returns {Object|Null} 1426 | */ 1427 | EpicEditor.prototype.getElement = function (name) { 1428 | var available = { 1429 | "container": this.element 1430 | , "wrapper": this.iframe.getElementById('epiceditor-wrapper') 1431 | , "wrapperIframe": this.iframeElement 1432 | , "editor": this.editorIframeDocument 1433 | , "editorIframe": this.editorIframe 1434 | , "previewer": this.previewerIframeDocument 1435 | , "previewerIframe": this.previewerIframe 1436 | } 1437 | 1438 | // Check that the given string is a possible option and verify the editor isn't unloaded 1439 | // without this, you'd be given a reference to an object that no longer exists in the DOM 1440 | if (!available[name] || this.is('unloaded')) { 1441 | return null; 1442 | } 1443 | else { 1444 | return available[name]; 1445 | } 1446 | } 1447 | 1448 | /** 1449 | * Returns a boolean of each "state" of the editor. For example "editor.is('loaded')" // returns true/false 1450 | * @param {String} what the state you want to check for 1451 | * @returns {Boolean} 1452 | */ 1453 | EpicEditor.prototype.is = function (what) { 1454 | var self = this; 1455 | switch (what) { 1456 | case 'loaded': 1457 | return self._eeState.loaded; 1458 | case 'unloaded': 1459 | return self._eeState.unloaded 1460 | case 'preview': 1461 | return self._eeState.preview 1462 | case 'edit': 1463 | return self._eeState.edit; 1464 | case 'fullscreen': 1465 | return self._eeState.fullscreen; 1466 | // TODO: This "works", but the tests are saying otherwise. Come back to this 1467 | // and figure out how to fix it. 1468 | // case 'focused': 1469 | // return document.activeElement == self.iframeElement; 1470 | default: 1471 | return false; 1472 | } 1473 | } 1474 | 1475 | /** 1476 | * Opens a file 1477 | * @param {string} name The name of the file you want to open 1478 | * @returns {object} EpicEditor will be returned 1479 | */ 1480 | EpicEditor.prototype.open = function (name) { 1481 | var self = this 1482 | , defaultContent = self.settings.file.defaultContent 1483 | , fileObj; 1484 | name = name || self.settings.file.name; 1485 | self.settings.file.name = name; 1486 | if (this._storage[self.settings.localStorageName]) { 1487 | fileObj = self.exportFile(name); 1488 | if (fileObj !== undefined) { 1489 | _setText(self.editor, fileObj); 1490 | self.emit('read'); 1491 | } 1492 | else { 1493 | _setText(self.editor, defaultContent); 1494 | self.save(); // ensure a save 1495 | self.emit('create'); 1496 | } 1497 | self.previewer.innerHTML = self.exportFile(null, 'html'); 1498 | self.emit('open'); 1499 | } 1500 | return this; 1501 | } 1502 | 1503 | /** 1504 | * Saves content for offline use 1505 | * @returns {object} EpicEditor will be returned 1506 | */ 1507 | EpicEditor.prototype.save = function (_isPreviewDraft, _isAuto) { 1508 | var self = this 1509 | , storage 1510 | , isUpdate = false 1511 | , isNew = false 1512 | , file = self.settings.file.name 1513 | , previewDraftName = '' 1514 | , data = this._storage[previewDraftName + self.settings.localStorageName] 1515 | , content = _getText(this.editor); 1516 | 1517 | if (_isPreviewDraft) { 1518 | previewDraftName = self._previewDraftLocation; 1519 | } 1520 | 1521 | // This could have been false but since we're manually saving 1522 | // we know it's save to start autoSaving again 1523 | this._canSave = true; 1524 | 1525 | // Guard against storage being wiped out without EpicEditor knowing 1526 | // TODO: Emit saving error - storage seems to have been wiped 1527 | if (data) { 1528 | storage = JSON.parse(this._storage[previewDraftName + self.settings.localStorageName]); 1529 | 1530 | // If the file doesn't exist we need to create it 1531 | if (storage[file] === undefined) { 1532 | storage[file] = self._defaultFileSchema(); 1533 | isNew = true; 1534 | } 1535 | 1536 | // If it does, we need to check if the content is different and 1537 | // if it is, send the update event and update the timestamp 1538 | else if (content !== storage[file].content) { 1539 | storage[file].modified = new Date(); 1540 | isUpdate = true; 1541 | } 1542 | //don't bother autosaving if the content hasn't actually changed 1543 | else if (_isAuto) { 1544 | return; 1545 | } 1546 | 1547 | storage[file].content = content; 1548 | this._storage[previewDraftName + self.settings.localStorageName] = JSON.stringify(storage); 1549 | 1550 | // If it's a new file, send a create event as well as a private one for 1551 | // use internally. 1552 | if (isNew) { 1553 | self.emit('create'); 1554 | self.emit('__create'); 1555 | } 1556 | 1557 | // After the content is actually changed, emit update so it emits the 1558 | // updated content. Also send a private event for interal use. 1559 | if (isUpdate) { 1560 | self.emit('update'); 1561 | self.emit('__update'); 1562 | } 1563 | 1564 | if (_isAuto) { 1565 | this.emit('autosave'); 1566 | } 1567 | else if (!_isPreviewDraft) { 1568 | this.emit('save'); 1569 | self.emit('__save'); 1570 | } 1571 | } 1572 | 1573 | return this; 1574 | } 1575 | 1576 | /** 1577 | * Removes a page 1578 | * @param {string} name The name of the file you want to remove from localStorage 1579 | * @returns {object} EpicEditor will be returned 1580 | */ 1581 | EpicEditor.prototype.remove = function (name) { 1582 | var self = this 1583 | , s; 1584 | name = name || self.settings.file.name; 1585 | 1586 | // If you're trying to delete a page you have open, block saving 1587 | if (name == self.settings.file.name) { 1588 | self._canSave = false; 1589 | } 1590 | 1591 | s = JSON.parse(this._storage[self.settings.localStorageName]); 1592 | delete s[name]; 1593 | this._storage[self.settings.localStorageName] = JSON.stringify(s); 1594 | this.emit('remove'); 1595 | return this; 1596 | }; 1597 | 1598 | /** 1599 | * Renames a file 1600 | * @param {string} oldName The old file name 1601 | * @param {string} newName The new file name 1602 | * @returns {object} EpicEditor will be returned 1603 | */ 1604 | EpicEditor.prototype.rename = function (oldName, newName) { 1605 | var self = this 1606 | , s = JSON.parse(this._storage[self.settings.localStorageName]); 1607 | s[newName] = s[oldName]; 1608 | delete s[oldName]; 1609 | this._storage[self.settings.localStorageName] = JSON.stringify(s); 1610 | self.open(newName); 1611 | return this; 1612 | }; 1613 | 1614 | /** 1615 | * Imports a file and it's contents and opens it 1616 | * @param {string} name The name of the file you want to import (will overwrite existing files!) 1617 | * @param {string} content Content of the file you want to import 1618 | * @param {string} kind The kind of file you want to import (TBI) 1619 | * @param {object} meta Meta data you want to save with your file. 1620 | * @returns {object} EpicEditor will be returned 1621 | */ 1622 | EpicEditor.prototype.importFile = function (name, content, kind, meta) { 1623 | var self = this; 1624 | 1625 | name = name || self.settings.file.name; 1626 | content = content || ''; 1627 | kind = kind || 'md'; 1628 | meta = meta || {}; 1629 | 1630 | // Set our current file to the new file and update the content 1631 | self.settings.file.name = name; 1632 | _setText(self.editor, content); 1633 | 1634 | self.save(); 1635 | 1636 | if (self.is('fullscreen')) { 1637 | self.preview(); 1638 | } 1639 | 1640 | //firefox has trouble with importing and working out the size right away 1641 | if (self.settings.autogrow) { 1642 | setTimeout(function () { 1643 | self._autogrow(); 1644 | }, 50); 1645 | } 1646 | 1647 | return this; 1648 | }; 1649 | 1650 | /** 1651 | * Gets the local filestore 1652 | * @param {string} name Name of the file in the store 1653 | * @returns {object|undefined} the local filestore, or a specific file in the store, if a name is given 1654 | */ 1655 | EpicEditor.prototype._getFileStore = function (name, _isPreviewDraft) { 1656 | var previewDraftName = '' 1657 | , store; 1658 | if (_isPreviewDraft) { 1659 | previewDraftName = this._previewDraftLocation; 1660 | } 1661 | store = JSON.parse(this._storage[previewDraftName + this.settings.localStorageName]); 1662 | if (name) { 1663 | return store[name]; 1664 | } 1665 | else { 1666 | return store; 1667 | } 1668 | } 1669 | 1670 | /** 1671 | * Exports a file as a string in a supported format 1672 | * @param {string} name Name of the file you want to export (case sensitive) 1673 | * @param {string} kind Kind of file you want the content in (currently supports html and text, default is the format the browser "wants") 1674 | * @returns {string|undefined} The content of the file in the content given or undefined if it doesn't exist 1675 | */ 1676 | EpicEditor.prototype.exportFile = function (name, kind, _isPreviewDraft) { 1677 | var self = this 1678 | , file 1679 | , content; 1680 | 1681 | name = name || self.settings.file.name; 1682 | kind = kind || 'text'; 1683 | 1684 | file = self._getFileStore(name, _isPreviewDraft); 1685 | 1686 | // If the file doesn't exist just return early with undefined 1687 | if (file === undefined) { 1688 | return; 1689 | } 1690 | 1691 | content = file.content; 1692 | 1693 | switch (kind) { 1694 | case 'html': 1695 | content = _sanitizeRawContent(content); 1696 | return self.settings.parser(content); 1697 | case 'text': 1698 | return _sanitizeRawContent(content); 1699 | case 'json': 1700 | file.content = _sanitizeRawContent(file.content); 1701 | return JSON.stringify(file); 1702 | case 'raw': 1703 | return content; 1704 | default: 1705 | return content; 1706 | } 1707 | } 1708 | 1709 | /** 1710 | * Gets the contents and metadata for files 1711 | * @param {string} name Name of the file whose data you want (case sensitive) 1712 | * @param {boolean} excludeContent whether the contents of files should be excluded 1713 | * @returns {object} An object with the names and data of every file, or just the data of one file if a name was given 1714 | */ 1715 | EpicEditor.prototype.getFiles = function (name, excludeContent) { 1716 | var file 1717 | , data = this._getFileStore(name); 1718 | 1719 | if (name) { 1720 | if (data !== undefined) { 1721 | if (excludeContent) { 1722 | delete data.content; 1723 | } 1724 | else { 1725 | data.content = _sanitizeRawContent(data.content); 1726 | } 1727 | } 1728 | return data; 1729 | } 1730 | else { 1731 | for (file in data) { 1732 | if (data.hasOwnProperty(file)) { 1733 | if (excludeContent) { 1734 | delete data[file].content; 1735 | } 1736 | else { 1737 | data[file].content = _sanitizeRawContent(data[file].content); 1738 | } 1739 | } 1740 | } 1741 | return data; 1742 | } 1743 | } 1744 | 1745 | // EVENTS 1746 | // TODO: Support for namespacing events like "preview.foo" 1747 | /** 1748 | * Sets up an event handler for a specified event 1749 | * @param {string} ev The event name 1750 | * @param {function} handler The callback to run when the event fires 1751 | * @returns {object} EpicEditor will be returned 1752 | */ 1753 | EpicEditor.prototype.on = function (ev, handler) { 1754 | var self = this; 1755 | if (!this.events[ev]) { 1756 | this.events[ev] = []; 1757 | } 1758 | this.events[ev].push(handler); 1759 | return self; 1760 | }; 1761 | 1762 | /** 1763 | * This will emit or "trigger" an event specified 1764 | * @param {string} ev The event name 1765 | * @param {any} data Any data you want to pass into the callback 1766 | * @returns {object} EpicEditor will be returned 1767 | */ 1768 | EpicEditor.prototype.emit = function (ev, data) { 1769 | var self = this 1770 | , x; 1771 | 1772 | data = data || self.getFiles(self.settings.file.name); 1773 | 1774 | if (!this.events[ev]) { 1775 | return; 1776 | } 1777 | 1778 | function invokeHandler(handler) { 1779 | handler.call(self, data); 1780 | } 1781 | 1782 | for (x = 0; x < self.events[ev].length; x++) { 1783 | invokeHandler(self.events[ev][x]); 1784 | } 1785 | 1786 | return self; 1787 | }; 1788 | 1789 | /** 1790 | * Will remove any listeners added from EpicEditor.on() 1791 | * @param {string} ev The event name 1792 | * @param {function} handler Handler to remove 1793 | * @returns {object} EpicEditor will be returned 1794 | */ 1795 | EpicEditor.prototype.removeListener = function (ev, handler) { 1796 | var self = this; 1797 | if (!handler) { 1798 | this.events[ev] = []; 1799 | return self; 1800 | } 1801 | if (!this.events[ev]) { 1802 | return self; 1803 | } 1804 | // Otherwise a handler and event exist, so take care of it 1805 | this.events[ev].splice(this.events[ev].indexOf(handler), 1); 1806 | return self; 1807 | } 1808 | 1809 | /** 1810 | * Handles autogrowing the editor 1811 | */ 1812 | EpicEditor.prototype._autogrow = function () { 1813 | var editorHeight 1814 | , newHeight 1815 | , minHeight 1816 | , maxHeight 1817 | , el 1818 | , style 1819 | , maxedOut = false; 1820 | 1821 | //autogrow in fullscreen is nonsensical 1822 | if (!this.is('fullscreen')) { 1823 | if (this.is('edit')) { 1824 | el = this.getElement('editor').documentElement; 1825 | } 1826 | else { 1827 | el = this.getElement('previewer').documentElement; 1828 | } 1829 | 1830 | editorHeight = _outerHeight(el); 1831 | newHeight = editorHeight; 1832 | 1833 | //handle minimum 1834 | minHeight = this.settings.autogrow.minHeight; 1835 | if (typeof minHeight === 'function') { 1836 | minHeight = minHeight(this); 1837 | } 1838 | 1839 | if (minHeight && newHeight < minHeight) { 1840 | newHeight = minHeight; 1841 | } 1842 | 1843 | //handle maximum 1844 | maxHeight = this.settings.autogrow.maxHeight; 1845 | if (typeof maxHeight === 'function') { 1846 | maxHeight = maxHeight(this); 1847 | } 1848 | 1849 | if (maxHeight && newHeight > maxHeight) { 1850 | newHeight = maxHeight; 1851 | maxedOut = true; 1852 | } 1853 | 1854 | if (maxedOut) { 1855 | this._fixScrollbars('auto'); 1856 | } else { 1857 | this._fixScrollbars('hidden'); 1858 | } 1859 | 1860 | //actual resize 1861 | if (newHeight != this.oldHeight) { 1862 | this.getElement('container').style.height = newHeight + 'px'; 1863 | this.reflow(); 1864 | if (this.settings.autogrow.scroll) { 1865 | window.scrollBy(0, newHeight - this.oldHeight); 1866 | } 1867 | this.oldHeight = newHeight; 1868 | } 1869 | } 1870 | } 1871 | 1872 | /** 1873 | * Shows or hides scrollbars based on the autogrow setting 1874 | * @param {string} forceSetting a value to force the overflow to 1875 | */ 1876 | EpicEditor.prototype._fixScrollbars = function (forceSetting) { 1877 | var setting; 1878 | if (this.settings.autogrow) { 1879 | setting = 'hidden'; 1880 | } 1881 | else { 1882 | setting = 'auto'; 1883 | } 1884 | setting = forceSetting || setting; 1885 | this.getElement('editor').documentElement.style.overflow = setting; 1886 | this.getElement('previewer').documentElement.style.overflow = setting; 1887 | } 1888 | 1889 | EpicEditor.version = '0.2.2'; 1890 | 1891 | // Used to store information to be shared across editors 1892 | EpicEditor._data = {}; 1893 | 1894 | if (typeof window.define === 'function' && window.define.amd) { 1895 | window.define(function () { return EpicEditor; }); 1896 | } else { 1897 | window.EpicEditor = EpicEditor; 1898 | } 1899 | })(window); 1900 | /** 1901 | * marked - a markdown parser 1902 | * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) 1903 | * https://github.com/chjj/marked 1904 | */ 1905 | 1906 | ;(function() { 1907 | 1908 | /** 1909 | * Block-Level Grammar 1910 | */ 1911 | 1912 | var block = { 1913 | newline: /^\n+/, 1914 | code: /^( {4}[^\n]+\n*)+/, 1915 | fences: noop, 1916 | hr: /^( *[-*_]){3,} *(?:\n+|$)/, 1917 | heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, 1918 | nptable: noop, 1919 | lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, 1920 | blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, 1921 | list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, 1922 | html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/, 1923 | def: /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, 1924 | table: noop, 1925 | paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/, 1926 | text: /^[^\n]+/ 1927 | }; 1928 | 1929 | block.bullet = /(?:[*+-]|\d+\.)/; 1930 | block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; 1931 | block.item = replace(block.item, 'gm') 1932 | (/bull/g, block.bullet) 1933 | (); 1934 | 1935 | block.list = replace(block.list) 1936 | (/bull/g, block.bullet) 1937 | ('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))') 1938 | ('def', '\\n+(?=' + block.def.source + ')') 1939 | (); 1940 | 1941 | block.blockquote = replace(block.blockquote) 1942 | ('def', block.def) 1943 | (); 1944 | 1945 | block._tag = '(?!(?:' 1946 | + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code' 1947 | + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo' 1948 | + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b'; 1949 | 1950 | block.html = replace(block.html) 1951 | ('comment', //) 1952 | ('closed', /<(tag)[\s\S]+?<\/\1>/) 1953 | ('closing', /])*?>/) 1954 | (/tag/g, block._tag) 1955 | (); 1956 | 1957 | block.paragraph = replace(block.paragraph) 1958 | ('hr', block.hr) 1959 | ('heading', block.heading) 1960 | ('lheading', block.lheading) 1961 | ('blockquote', block.blockquote) 1962 | ('tag', '<' + block._tag) 1963 | ('def', block.def) 1964 | (); 1965 | 1966 | /** 1967 | * Normal Block Grammar 1968 | */ 1969 | 1970 | block.normal = merge({}, block); 1971 | 1972 | /** 1973 | * GFM Block Grammar 1974 | */ 1975 | 1976 | block.gfm = merge({}, block.normal, { 1977 | fences: /^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/, 1978 | paragraph: /^/ 1979 | }); 1980 | 1981 | block.gfm.paragraph = replace(block.paragraph) 1982 | ('(?!', '(?!' 1983 | + block.gfm.fences.source.replace('\\1', '\\2') + '|' 1984 | + block.list.source.replace('\\1', '\\3') + '|') 1985 | (); 1986 | 1987 | /** 1988 | * GFM + Tables Block Grammar 1989 | */ 1990 | 1991 | block.tables = merge({}, block.gfm, { 1992 | nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, 1993 | table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/ 1994 | }); 1995 | 1996 | /** 1997 | * Block Lexer 1998 | */ 1999 | 2000 | function Lexer(options) { 2001 | this.tokens = []; 2002 | this.tokens.links = {}; 2003 | this.options = options || marked.defaults; 2004 | this.rules = block.normal; 2005 | 2006 | if (this.options.gfm) { 2007 | if (this.options.tables) { 2008 | this.rules = block.tables; 2009 | } else { 2010 | this.rules = block.gfm; 2011 | } 2012 | } 2013 | } 2014 | 2015 | /** 2016 | * Expose Block Rules 2017 | */ 2018 | 2019 | Lexer.rules = block; 2020 | 2021 | /** 2022 | * Static Lex Method 2023 | */ 2024 | 2025 | Lexer.lex = function(src, options) { 2026 | var lexer = new Lexer(options); 2027 | return lexer.lex(src); 2028 | }; 2029 | 2030 | /** 2031 | * Preprocessing 2032 | */ 2033 | 2034 | Lexer.prototype.lex = function(src) { 2035 | src = src 2036 | .replace(/\r\n|\r/g, '\n') 2037 | .replace(/\t/g, ' ') 2038 | .replace(/\u00a0/g, ' ') 2039 | .replace(/\u2424/g, '\n'); 2040 | 2041 | return this.token(src, true); 2042 | }; 2043 | 2044 | /** 2045 | * Lexing 2046 | */ 2047 | 2048 | Lexer.prototype.token = function(src, top, bq) { 2049 | var src = src.replace(/^ +$/gm, '') 2050 | , next 2051 | , loose 2052 | , cap 2053 | , bull 2054 | , b 2055 | , item 2056 | , space 2057 | , i 2058 | , l; 2059 | 2060 | while (src) { 2061 | // newline 2062 | if (cap = this.rules.newline.exec(src)) { 2063 | src = src.substring(cap[0].length); 2064 | if (cap[0].length > 1) { 2065 | this.tokens.push({ 2066 | type: 'space' 2067 | }); 2068 | } 2069 | } 2070 | 2071 | // code 2072 | if (cap = this.rules.code.exec(src)) { 2073 | src = src.substring(cap[0].length); 2074 | cap = cap[0].replace(/^ {4}/gm, ''); 2075 | this.tokens.push({ 2076 | type: 'code', 2077 | text: !this.options.pedantic 2078 | ? cap.replace(/\n+$/, '') 2079 | : cap 2080 | }); 2081 | continue; 2082 | } 2083 | 2084 | // fences (gfm) 2085 | if (cap = this.rules.fences.exec(src)) { 2086 | src = src.substring(cap[0].length); 2087 | this.tokens.push({ 2088 | type: 'code', 2089 | lang: cap[2], 2090 | text: cap[3] 2091 | }); 2092 | continue; 2093 | } 2094 | 2095 | // heading 2096 | if (cap = this.rules.heading.exec(src)) { 2097 | src = src.substring(cap[0].length); 2098 | this.tokens.push({ 2099 | type: 'heading', 2100 | depth: cap[1].length, 2101 | text: cap[2] 2102 | }); 2103 | continue; 2104 | } 2105 | 2106 | // table no leading pipe (gfm) 2107 | if (top && (cap = this.rules.nptable.exec(src))) { 2108 | src = src.substring(cap[0].length); 2109 | 2110 | item = { 2111 | type: 'table', 2112 | header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), 2113 | align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), 2114 | cells: cap[3].replace(/\n$/, '').split('\n') 2115 | }; 2116 | 2117 | for (i = 0; i < item.align.length; i++) { 2118 | if (/^ *-+: *$/.test(item.align[i])) { 2119 | item.align[i] = 'right'; 2120 | } else if (/^ *:-+: *$/.test(item.align[i])) { 2121 | item.align[i] = 'center'; 2122 | } else if (/^ *:-+ *$/.test(item.align[i])) { 2123 | item.align[i] = 'left'; 2124 | } else { 2125 | item.align[i] = null; 2126 | } 2127 | } 2128 | 2129 | for (i = 0; i < item.cells.length; i++) { 2130 | item.cells[i] = item.cells[i].split(/ *\| */); 2131 | } 2132 | 2133 | this.tokens.push(item); 2134 | 2135 | continue; 2136 | } 2137 | 2138 | // lheading 2139 | if (cap = this.rules.lheading.exec(src)) { 2140 | src = src.substring(cap[0].length); 2141 | this.tokens.push({ 2142 | type: 'heading', 2143 | depth: cap[2] === '=' ? 1 : 2, 2144 | text: cap[1] 2145 | }); 2146 | continue; 2147 | } 2148 | 2149 | // hr 2150 | if (cap = this.rules.hr.exec(src)) { 2151 | src = src.substring(cap[0].length); 2152 | this.tokens.push({ 2153 | type: 'hr' 2154 | }); 2155 | continue; 2156 | } 2157 | 2158 | // blockquote 2159 | if (cap = this.rules.blockquote.exec(src)) { 2160 | src = src.substring(cap[0].length); 2161 | 2162 | this.tokens.push({ 2163 | type: 'blockquote_start' 2164 | }); 2165 | 2166 | cap = cap[0].replace(/^ *> ?/gm, ''); 2167 | 2168 | // Pass `top` to keep the current 2169 | // "toplevel" state. This is exactly 2170 | // how markdown.pl works. 2171 | this.token(cap, top, true); 2172 | 2173 | this.tokens.push({ 2174 | type: 'blockquote_end' 2175 | }); 2176 | 2177 | continue; 2178 | } 2179 | 2180 | // list 2181 | if (cap = this.rules.list.exec(src)) { 2182 | src = src.substring(cap[0].length); 2183 | bull = cap[2]; 2184 | 2185 | this.tokens.push({ 2186 | type: 'list_start', 2187 | ordered: bull.length > 1 2188 | }); 2189 | 2190 | // Get each top-level item. 2191 | cap = cap[0].match(this.rules.item); 2192 | 2193 | next = false; 2194 | l = cap.length; 2195 | i = 0; 2196 | 2197 | for (; i < l; i++) { 2198 | item = cap[i]; 2199 | 2200 | // Remove the list item's bullet 2201 | // so it is seen as the next token. 2202 | space = item.length; 2203 | item = item.replace(/^ *([*+-]|\d+\.) +/, ''); 2204 | 2205 | // Outdent whatever the 2206 | // list item contains. Hacky. 2207 | if (~item.indexOf('\n ')) { 2208 | space -= item.length; 2209 | item = !this.options.pedantic 2210 | ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') 2211 | : item.replace(/^ {1,4}/gm, ''); 2212 | } 2213 | 2214 | // Determine whether the next list item belongs here. 2215 | // Backpedal if it does not belong in this list. 2216 | if (this.options.smartLists && i !== l - 1) { 2217 | b = block.bullet.exec(cap[i + 1])[0]; 2218 | if (bull !== b && !(bull.length > 1 && b.length > 1)) { 2219 | src = cap.slice(i + 1).join('\n') + src; 2220 | i = l - 1; 2221 | } 2222 | } 2223 | 2224 | // Determine whether item is loose or not. 2225 | // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ 2226 | // for discount behavior. 2227 | loose = next || /\n\n(?!\s*$)/.test(item); 2228 | if (i !== l - 1) { 2229 | next = item.charAt(item.length - 1) === '\n'; 2230 | if (!loose) loose = next; 2231 | } 2232 | 2233 | this.tokens.push({ 2234 | type: loose 2235 | ? 'loose_item_start' 2236 | : 'list_item_start' 2237 | }); 2238 | 2239 | // Recurse. 2240 | this.token(item, false, bq); 2241 | 2242 | this.tokens.push({ 2243 | type: 'list_item_end' 2244 | }); 2245 | } 2246 | 2247 | this.tokens.push({ 2248 | type: 'list_end' 2249 | }); 2250 | 2251 | continue; 2252 | } 2253 | 2254 | // html 2255 | if (cap = this.rules.html.exec(src)) { 2256 | src = src.substring(cap[0].length); 2257 | this.tokens.push({ 2258 | type: this.options.sanitize 2259 | ? 'paragraph' 2260 | : 'html', 2261 | pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style', 2262 | text: cap[0] 2263 | }); 2264 | continue; 2265 | } 2266 | 2267 | // def 2268 | if ((!bq && top) && (cap = this.rules.def.exec(src))) { 2269 | src = src.substring(cap[0].length); 2270 | this.tokens.links[cap[1].toLowerCase()] = { 2271 | href: cap[2], 2272 | title: cap[3] 2273 | }; 2274 | continue; 2275 | } 2276 | 2277 | // table (gfm) 2278 | if (top && (cap = this.rules.table.exec(src))) { 2279 | src = src.substring(cap[0].length); 2280 | 2281 | item = { 2282 | type: 'table', 2283 | header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), 2284 | align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), 2285 | cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') 2286 | }; 2287 | 2288 | for (i = 0; i < item.align.length; i++) { 2289 | if (/^ *-+: *$/.test(item.align[i])) { 2290 | item.align[i] = 'right'; 2291 | } else if (/^ *:-+: *$/.test(item.align[i])) { 2292 | item.align[i] = 'center'; 2293 | } else if (/^ *:-+ *$/.test(item.align[i])) { 2294 | item.align[i] = 'left'; 2295 | } else { 2296 | item.align[i] = null; 2297 | } 2298 | } 2299 | 2300 | for (i = 0; i < item.cells.length; i++) { 2301 | item.cells[i] = item.cells[i] 2302 | .replace(/^ *\| *| *\| *$/g, '') 2303 | .split(/ *\| */); 2304 | } 2305 | 2306 | this.tokens.push(item); 2307 | 2308 | continue; 2309 | } 2310 | 2311 | // top-level paragraph 2312 | if (top && (cap = this.rules.paragraph.exec(src))) { 2313 | src = src.substring(cap[0].length); 2314 | this.tokens.push({ 2315 | type: 'paragraph', 2316 | text: cap[1].charAt(cap[1].length - 1) === '\n' 2317 | ? cap[1].slice(0, -1) 2318 | : cap[1] 2319 | }); 2320 | continue; 2321 | } 2322 | 2323 | // text 2324 | if (cap = this.rules.text.exec(src)) { 2325 | // Top-level should never reach here. 2326 | src = src.substring(cap[0].length); 2327 | this.tokens.push({ 2328 | type: 'text', 2329 | text: cap[0] 2330 | }); 2331 | continue; 2332 | } 2333 | 2334 | if (src) { 2335 | throw new 2336 | Error('Infinite loop on byte: ' + src.charCodeAt(0)); 2337 | } 2338 | } 2339 | 2340 | return this.tokens; 2341 | }; 2342 | 2343 | /** 2344 | * Inline-Level Grammar 2345 | */ 2346 | 2347 | var inline = { 2348 | escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, 2349 | autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, 2350 | url: noop, 2351 | tag: /^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/, 2352 | link: /^!?\[(inside)\]\(href\)/, 2353 | reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, 2354 | nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, 2355 | strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, 2356 | em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, 2357 | code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, 2358 | br: /^ {2,}\n(?!\s*$)/, 2359 | del: noop, 2360 | text: /^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/; 2365 | 2366 | inline.link = replace(inline.link) 2367 | ('inside', inline._inside) 2368 | ('href', inline._href) 2369 | (); 2370 | 2371 | inline.reflink = replace(inline.reflink) 2372 | ('inside', inline._inside) 2373 | (); 2374 | 2375 | /** 2376 | * Normal Inline Grammar 2377 | */ 2378 | 2379 | inline.normal = merge({}, inline); 2380 | 2381 | /** 2382 | * Pedantic Inline Grammar 2383 | */ 2384 | 2385 | inline.pedantic = merge({}, inline.normal, { 2386 | strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, 2387 | em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/ 2388 | }); 2389 | 2390 | /** 2391 | * GFM Inline Grammar 2392 | */ 2393 | 2394 | inline.gfm = merge({}, inline.normal, { 2395 | escape: replace(inline.escape)('])', '~|])')(), 2396 | url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/, 2397 | del: /^~~(?=\S)([\s\S]*?\S)~~/, 2398 | text: replace(inline.text) 2399 | (']|', '~]|') 2400 | ('|', '|https?://|') 2401 | () 2402 | }); 2403 | 2404 | /** 2405 | * GFM + Line Breaks Inline Grammar 2406 | */ 2407 | 2408 | inline.breaks = merge({}, inline.gfm, { 2409 | br: replace(inline.br)('{2,}', '*')(), 2410 | text: replace(inline.gfm.text)('{2,}', '*')() 2411 | }); 2412 | 2413 | /** 2414 | * Inline Lexer & Compiler 2415 | */ 2416 | 2417 | function InlineLexer(links, options) { 2418 | this.options = options || marked.defaults; 2419 | this.links = links; 2420 | this.rules = inline.normal; 2421 | this.renderer = this.options.renderer || new Renderer; 2422 | this.renderer.options = this.options; 2423 | 2424 | if (!this.links) { 2425 | throw new 2426 | Error('Tokens array requires a `links` property.'); 2427 | } 2428 | 2429 | if (this.options.gfm) { 2430 | if (this.options.breaks) { 2431 | this.rules = inline.breaks; 2432 | } else { 2433 | this.rules = inline.gfm; 2434 | } 2435 | } else if (this.options.pedantic) { 2436 | this.rules = inline.pedantic; 2437 | } 2438 | } 2439 | 2440 | /** 2441 | * Expose Inline Rules 2442 | */ 2443 | 2444 | InlineLexer.rules = inline; 2445 | 2446 | /** 2447 | * Static Lexing/Compiling Method 2448 | */ 2449 | 2450 | InlineLexer.output = function(src, links, options) { 2451 | var inline = new InlineLexer(links, options); 2452 | return inline.output(src); 2453 | }; 2454 | 2455 | /** 2456 | * Lexing/Compiling 2457 | */ 2458 | 2459 | InlineLexer.prototype.output = function(src) { 2460 | var out = '' 2461 | , link 2462 | , text 2463 | , href 2464 | , cap; 2465 | 2466 | while (src) { 2467 | // escape 2468 | if (cap = this.rules.escape.exec(src)) { 2469 | src = src.substring(cap[0].length); 2470 | out += cap[1]; 2471 | continue; 2472 | } 2473 | 2474 | // autolink 2475 | if (cap = this.rules.autolink.exec(src)) { 2476 | src = src.substring(cap[0].length); 2477 | if (cap[2] === '@') { 2478 | text = cap[1].charAt(6) === ':' 2479 | ? this.mangle(cap[1].substring(7)) 2480 | : this.mangle(cap[1]); 2481 | href = this.mangle('mailto:') + text; 2482 | } else { 2483 | text = escape(cap[1]); 2484 | href = text; 2485 | } 2486 | out += this.renderer.link(href, null, text); 2487 | continue; 2488 | } 2489 | 2490 | // url (gfm) 2491 | if (!this.inLink && (cap = this.rules.url.exec(src))) { 2492 | src = src.substring(cap[0].length); 2493 | text = escape(cap[1]); 2494 | href = text; 2495 | out += this.renderer.link(href, null, text); 2496 | continue; 2497 | } 2498 | 2499 | // tag 2500 | if (cap = this.rules.tag.exec(src)) { 2501 | if (!this.inLink && /^/i.test(cap[0])) { 2504 | this.inLink = false; 2505 | } 2506 | src = src.substring(cap[0].length); 2507 | out += this.options.sanitize 2508 | ? escape(cap[0]) 2509 | : cap[0]; 2510 | continue; 2511 | } 2512 | 2513 | // link 2514 | if (cap = this.rules.link.exec(src)) { 2515 | src = src.substring(cap[0].length); 2516 | this.inLink = true; 2517 | out += this.outputLink(cap, { 2518 | href: cap[2], 2519 | title: cap[3] 2520 | }); 2521 | this.inLink = false; 2522 | continue; 2523 | } 2524 | 2525 | // reflink, nolink 2526 | if ((cap = this.rules.reflink.exec(src)) 2527 | || (cap = this.rules.nolink.exec(src))) { 2528 | src = src.substring(cap[0].length); 2529 | link = (cap[2] || cap[1]).replace(/\s+/g, ' '); 2530 | link = this.links[link.toLowerCase()]; 2531 | if (!link || !link.href) { 2532 | out += cap[0].charAt(0); 2533 | src = cap[0].substring(1) + src; 2534 | continue; 2535 | } 2536 | this.inLink = true; 2537 | out += this.outputLink(cap, link); 2538 | this.inLink = false; 2539 | continue; 2540 | } 2541 | 2542 | // strong 2543 | if (cap = this.rules.strong.exec(src)) { 2544 | src = src.substring(cap[0].length); 2545 | out += this.renderer.strong(this.output(cap[2] || cap[1])); 2546 | continue; 2547 | } 2548 | 2549 | // em 2550 | if (cap = this.rules.em.exec(src)) { 2551 | src = src.substring(cap[0].length); 2552 | out += this.renderer.em(this.output(cap[2] || cap[1])); 2553 | continue; 2554 | } 2555 | 2556 | // code 2557 | if (cap = this.rules.code.exec(src)) { 2558 | src = src.substring(cap[0].length); 2559 | out += this.renderer.codespan(escape(cap[2], true)); 2560 | continue; 2561 | } 2562 | 2563 | // br 2564 | if (cap = this.rules.br.exec(src)) { 2565 | src = src.substring(cap[0].length); 2566 | out += this.renderer.br(); 2567 | continue; 2568 | } 2569 | 2570 | // del (gfm) 2571 | if (cap = this.rules.del.exec(src)) { 2572 | src = src.substring(cap[0].length); 2573 | out += this.renderer.del(this.output(cap[1])); 2574 | continue; 2575 | } 2576 | 2577 | // text 2578 | if (cap = this.rules.text.exec(src)) { 2579 | src = src.substring(cap[0].length); 2580 | out += escape(this.smartypants(cap[0])); 2581 | continue; 2582 | } 2583 | 2584 | if (src) { 2585 | throw new 2586 | Error('Infinite loop on byte: ' + src.charCodeAt(0)); 2587 | } 2588 | } 2589 | 2590 | return out; 2591 | }; 2592 | 2593 | /** 2594 | * Compile Link 2595 | */ 2596 | 2597 | InlineLexer.prototype.outputLink = function(cap, link) { 2598 | var href = escape(link.href) 2599 | , title = link.title ? escape(link.title) : null; 2600 | 2601 | return cap[0].charAt(0) !== '!' 2602 | ? this.renderer.link(href, title, this.output(cap[1])) 2603 | : this.renderer.image(href, title, escape(cap[1])); 2604 | }; 2605 | 2606 | /** 2607 | * Smartypants Transformations 2608 | */ 2609 | 2610 | InlineLexer.prototype.smartypants = function(text) { 2611 | if (!this.options.smartypants) return text; 2612 | return text 2613 | // em-dashes 2614 | .replace(/--/g, '\u2014') 2615 | // opening singles 2616 | .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018') 2617 | // closing singles & apostrophes 2618 | .replace(/'/g, '\u2019') 2619 | // opening doubles 2620 | .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c') 2621 | // closing doubles 2622 | .replace(/"/g, '\u201d') 2623 | // ellipses 2624 | .replace(/\.{3}/g, '\u2026'); 2625 | }; 2626 | 2627 | /** 2628 | * Mangle Links 2629 | */ 2630 | 2631 | InlineLexer.prototype.mangle = function(text) { 2632 | var out = '' 2633 | , l = text.length 2634 | , i = 0 2635 | , ch; 2636 | 2637 | for (; i < l; i++) { 2638 | ch = text.charCodeAt(i); 2639 | if (Math.random() > 0.5) { 2640 | ch = 'x' + ch.toString(16); 2641 | } 2642 | out += '&#' + ch + ';'; 2643 | } 2644 | 2645 | return out; 2646 | }; 2647 | 2648 | /** 2649 | * Renderer 2650 | */ 2651 | 2652 | function Renderer(options) { 2653 | this.options = options || {}; 2654 | } 2655 | 2656 | Renderer.prototype.code = function(code, lang, escaped) { 2657 | if (this.options.highlight) { 2658 | var out = this.options.highlight(code, lang); 2659 | if (out != null && out !== code) { 2660 | escaped = true; 2661 | code = out; 2662 | } 2663 | } 2664 | 2665 | if (!lang) { 2666 | return '
'
2667 |       + (escaped ? code : escape(code, true))
2668 |       + '\n
'; 2669 | } 2670 | 2671 | return '
'
2675 |     + (escaped ? code : escape(code, true))
2676 |     + '\n
\n'; 2677 | }; 2678 | 2679 | Renderer.prototype.blockquote = function(quote) { 2680 | return '
\n' + quote + '
\n'; 2681 | }; 2682 | 2683 | Renderer.prototype.html = function(html) { 2684 | return html; 2685 | }; 2686 | 2687 | Renderer.prototype.heading = function(text, level, raw) { 2688 | return '' 2694 | + text 2695 | + '\n'; 2698 | }; 2699 | 2700 | Renderer.prototype.hr = function() { 2701 | return this.options.xhtml ? '
\n' : '
\n'; 2702 | }; 2703 | 2704 | Renderer.prototype.list = function(body, ordered) { 2705 | var type = ordered ? 'ol' : 'ul'; 2706 | return '<' + type + '>\n' + body + '\n'; 2707 | }; 2708 | 2709 | Renderer.prototype.listitem = function(text) { 2710 | return '
  • ' + text + '
  • \n'; 2711 | }; 2712 | 2713 | Renderer.prototype.paragraph = function(text) { 2714 | return '

    ' + text + '

    \n'; 2715 | }; 2716 | 2717 | Renderer.prototype.table = function(header, body) { 2718 | return '\n' 2719 | + '\n' 2720 | + header 2721 | + '\n' 2722 | + '\n' 2723 | + body 2724 | + '\n' 2725 | + '
    \n'; 2726 | }; 2727 | 2728 | Renderer.prototype.tablerow = function(content) { 2729 | return '\n' + content + '\n'; 2730 | }; 2731 | 2732 | Renderer.prototype.tablecell = function(content, flags) { 2733 | var type = flags.header ? 'th' : 'td'; 2734 | var tag = flags.align 2735 | ? '<' + type + ' style="text-align:' + flags.align + '">' 2736 | : '<' + type + '>'; 2737 | return tag + content + '\n'; 2738 | }; 2739 | 2740 | // span level renderer 2741 | Renderer.prototype.strong = function(text) { 2742 | return '' + text + ''; 2743 | }; 2744 | 2745 | Renderer.prototype.em = function(text) { 2746 | return '' + text + ''; 2747 | }; 2748 | 2749 | Renderer.prototype.codespan = function(text) { 2750 | return '' + text + ''; 2751 | }; 2752 | 2753 | Renderer.prototype.br = function() { 2754 | return this.options.xhtml ? '
    ' : '
    '; 2755 | }; 2756 | 2757 | Renderer.prototype.del = function(text) { 2758 | return '' + text + ''; 2759 | }; 2760 | 2761 | Renderer.prototype.link = function(href, title, text) { 2762 | if (this.options.sanitize) { 2763 | try { 2764 | var prot = decodeURIComponent(unescape(href)) 2765 | .replace(/[^\w:]/g, '') 2766 | .toLowerCase(); 2767 | } catch (e) { 2768 | return ''; 2769 | } 2770 | if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0) { 2771 | return ''; 2772 | } 2773 | } 2774 | var out = '
    '; 2779 | return out; 2780 | }; 2781 | 2782 | Renderer.prototype.image = function(href, title, text) { 2783 | var out = '' + text + '' : '>'; 2788 | return out; 2789 | }; 2790 | 2791 | /** 2792 | * Parsing & Compiling 2793 | */ 2794 | 2795 | function Parser(options) { 2796 | this.tokens = []; 2797 | this.token = null; 2798 | this.options = options || marked.defaults; 2799 | this.options.renderer = this.options.renderer || new Renderer; 2800 | this.renderer = this.options.renderer; 2801 | this.renderer.options = this.options; 2802 | } 2803 | 2804 | /** 2805 | * Static Parse Method 2806 | */ 2807 | 2808 | Parser.parse = function(src, options, renderer) { 2809 | var parser = new Parser(options, renderer); 2810 | return parser.parse(src); 2811 | }; 2812 | 2813 | /** 2814 | * Parse Loop 2815 | */ 2816 | 2817 | Parser.prototype.parse = function(src) { 2818 | this.inline = new InlineLexer(src.links, this.options, this.renderer); 2819 | this.tokens = src.reverse(); 2820 | 2821 | var out = ''; 2822 | while (this.next()) { 2823 | out += this.tok(); 2824 | } 2825 | 2826 | return out; 2827 | }; 2828 | 2829 | /** 2830 | * Next Token 2831 | */ 2832 | 2833 | Parser.prototype.next = function() { 2834 | return this.token = this.tokens.pop(); 2835 | }; 2836 | 2837 | /** 2838 | * Preview Next Token 2839 | */ 2840 | 2841 | Parser.prototype.peek = function() { 2842 | return this.tokens[this.tokens.length - 1] || 0; 2843 | }; 2844 | 2845 | /** 2846 | * Parse Text Tokens 2847 | */ 2848 | 2849 | Parser.prototype.parseText = function() { 2850 | var body = this.token.text; 2851 | 2852 | while (this.peek().type === 'text') { 2853 | body += '\n' + this.next().text; 2854 | } 2855 | 2856 | return this.inline.output(body); 2857 | }; 2858 | 2859 | /** 2860 | * Parse Current Token 2861 | */ 2862 | 2863 | Parser.prototype.tok = function() { 2864 | switch (this.token.type) { 2865 | case 'space': { 2866 | return ''; 2867 | } 2868 | case 'hr': { 2869 | return this.renderer.hr(); 2870 | } 2871 | case 'heading': { 2872 | return this.renderer.heading( 2873 | this.inline.output(this.token.text), 2874 | this.token.depth, 2875 | this.token.text); 2876 | } 2877 | case 'code': { 2878 | return this.renderer.code(this.token.text, 2879 | this.token.lang, 2880 | this.token.escaped); 2881 | } 2882 | case 'table': { 2883 | var header = '' 2884 | , body = '' 2885 | , i 2886 | , row 2887 | , cell 2888 | , flags 2889 | , j; 2890 | 2891 | // header 2892 | cell = ''; 2893 | for (i = 0; i < this.token.header.length; i++) { 2894 | flags = { header: true, align: this.token.align[i] }; 2895 | cell += this.renderer.tablecell( 2896 | this.inline.output(this.token.header[i]), 2897 | { header: true, align: this.token.align[i] } 2898 | ); 2899 | } 2900 | header += this.renderer.tablerow(cell); 2901 | 2902 | for (i = 0; i < this.token.cells.length; i++) { 2903 | row = this.token.cells[i]; 2904 | 2905 | cell = ''; 2906 | for (j = 0; j < row.length; j++) { 2907 | cell += this.renderer.tablecell( 2908 | this.inline.output(row[j]), 2909 | { header: false, align: this.token.align[j] } 2910 | ); 2911 | } 2912 | 2913 | body += this.renderer.tablerow(cell); 2914 | } 2915 | return this.renderer.table(header, body); 2916 | } 2917 | case 'blockquote_start': { 2918 | var body = ''; 2919 | 2920 | while (this.next().type !== 'blockquote_end') { 2921 | body += this.tok(); 2922 | } 2923 | 2924 | return this.renderer.blockquote(body); 2925 | } 2926 | case 'list_start': { 2927 | var body = '' 2928 | , ordered = this.token.ordered; 2929 | 2930 | while (this.next().type !== 'list_end') { 2931 | body += this.tok(); 2932 | } 2933 | 2934 | return this.renderer.list(body, ordered); 2935 | } 2936 | case 'list_item_start': { 2937 | var body = ''; 2938 | 2939 | while (this.next().type !== 'list_item_end') { 2940 | body += this.token.type === 'text' 2941 | ? this.parseText() 2942 | : this.tok(); 2943 | } 2944 | 2945 | return this.renderer.listitem(body); 2946 | } 2947 | case 'loose_item_start': { 2948 | var body = ''; 2949 | 2950 | while (this.next().type !== 'list_item_end') { 2951 | body += this.tok(); 2952 | } 2953 | 2954 | return this.renderer.listitem(body); 2955 | } 2956 | case 'html': { 2957 | var html = !this.token.pre && !this.options.pedantic 2958 | ? this.inline.output(this.token.text) 2959 | : this.token.text; 2960 | return this.renderer.html(html); 2961 | } 2962 | case 'paragraph': { 2963 | return this.renderer.paragraph(this.inline.output(this.token.text)); 2964 | } 2965 | case 'text': { 2966 | return this.renderer.paragraph(this.parseText()); 2967 | } 2968 | } 2969 | }; 2970 | 2971 | /** 2972 | * Helpers 2973 | */ 2974 | 2975 | function escape(html, encode) { 2976 | return html 2977 | .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') 2978 | .replace(//g, '>') 2980 | .replace(/"/g, '"') 2981 | .replace(/'/g, '''); 2982 | } 2983 | 2984 | function unescape(html) { 2985 | return html.replace(/&([#\w]+);/g, function(_, n) { 2986 | n = n.toLowerCase(); 2987 | if (n === 'colon') return ':'; 2988 | if (n.charAt(0) === '#') { 2989 | return n.charAt(1) === 'x' 2990 | ? String.fromCharCode(parseInt(n.substring(2), 16)) 2991 | : String.fromCharCode(+n.substring(1)); 2992 | } 2993 | return ''; 2994 | }); 2995 | } 2996 | 2997 | function replace(regex, opt) { 2998 | regex = regex.source; 2999 | opt = opt || ''; 3000 | return function self(name, val) { 3001 | if (!name) return new RegExp(regex, opt); 3002 | val = val.source || val; 3003 | val = val.replace(/(^|[^\[])\^/g, '$1'); 3004 | regex = regex.replace(name, val); 3005 | return self; 3006 | }; 3007 | } 3008 | 3009 | function noop() {} 3010 | noop.exec = noop; 3011 | 3012 | function merge(obj) { 3013 | var i = 1 3014 | , target 3015 | , key; 3016 | 3017 | for (; i < arguments.length; i++) { 3018 | target = arguments[i]; 3019 | for (key in target) { 3020 | if (Object.prototype.hasOwnProperty.call(target, key)) { 3021 | obj[key] = target[key]; 3022 | } 3023 | } 3024 | } 3025 | 3026 | return obj; 3027 | } 3028 | 3029 | 3030 | /** 3031 | * Marked 3032 | */ 3033 | 3034 | function marked(src, opt, callback) { 3035 | if (callback || typeof opt === 'function') { 3036 | if (!callback) { 3037 | callback = opt; 3038 | opt = null; 3039 | } 3040 | 3041 | opt = merge({}, marked.defaults, opt || {}); 3042 | 3043 | var highlight = opt.highlight 3044 | , tokens 3045 | , pending 3046 | , i = 0; 3047 | 3048 | try { 3049 | tokens = Lexer.lex(src, opt) 3050 | } catch (e) { 3051 | return callback(e); 3052 | } 3053 | 3054 | pending = tokens.length; 3055 | 3056 | var done = function(err) { 3057 | if (err) { 3058 | opt.highlight = highlight; 3059 | return callback(err); 3060 | } 3061 | 3062 | var out; 3063 | 3064 | try { 3065 | out = Parser.parse(tokens, opt); 3066 | } catch (e) { 3067 | err = e; 3068 | } 3069 | 3070 | opt.highlight = highlight; 3071 | 3072 | return err 3073 | ? callback(err) 3074 | : callback(null, out); 3075 | }; 3076 | 3077 | if (!highlight || highlight.length < 3) { 3078 | return done(); 3079 | } 3080 | 3081 | delete opt.highlight; 3082 | 3083 | if (!pending) return done(); 3084 | 3085 | for (; i < tokens.length; i++) { 3086 | (function(token) { 3087 | if (token.type !== 'code') { 3088 | return --pending || done(); 3089 | } 3090 | return highlight(token.text, token.lang, function(err, code) { 3091 | if (err) return done(err); 3092 | if (code == null || code === token.text) { 3093 | return --pending || done(); 3094 | } 3095 | token.text = code; 3096 | token.escaped = true; 3097 | --pending || done(); 3098 | }); 3099 | })(tokens[i]); 3100 | } 3101 | 3102 | return; 3103 | } 3104 | try { 3105 | if (opt) opt = merge({}, marked.defaults, opt); 3106 | return Parser.parse(Lexer.lex(src, opt), opt); 3107 | } catch (e) { 3108 | e.message += '\nPlease report this to https://github.com/chjj/marked.'; 3109 | if ((opt || marked.defaults).silent) { 3110 | return '

    An error occured:

    '
    3111 |         + escape(e.message + '', true)
    3112 |         + '
    '; 3113 | } 3114 | throw e; 3115 | } 3116 | } 3117 | 3118 | /** 3119 | * Options 3120 | */ 3121 | 3122 | marked.options = 3123 | marked.setOptions = function(opt) { 3124 | merge(marked.defaults, opt); 3125 | return marked; 3126 | }; 3127 | 3128 | marked.defaults = { 3129 | gfm: true, 3130 | tables: true, 3131 | breaks: false, 3132 | pedantic: false, 3133 | sanitize: false, 3134 | smartLists: false, 3135 | silent: false, 3136 | highlight: null, 3137 | langPrefix: 'lang-', 3138 | smartypants: false, 3139 | headerPrefix: '', 3140 | renderer: new Renderer, 3141 | xhtml: false 3142 | }; 3143 | 3144 | /** 3145 | * Expose 3146 | */ 3147 | 3148 | marked.Parser = Parser; 3149 | marked.parser = Parser.parse; 3150 | 3151 | marked.Renderer = Renderer; 3152 | 3153 | marked.Lexer = Lexer; 3154 | marked.lexer = Lexer.lex; 3155 | 3156 | marked.InlineLexer = InlineLexer; 3157 | marked.inlineLexer = InlineLexer.output; 3158 | 3159 | marked.parse = marked; 3160 | 3161 | if (typeof module !== 'undefined' && typeof exports === 'object') { 3162 | module.exports = marked; 3163 | } else if (typeof define === 'function' && define.amd) { 3164 | define(function() { return marked; }); 3165 | } else { 3166 | this.marked = marked; 3167 | } 3168 | 3169 | }).call(function() { 3170 | return this || (typeof window !== 'undefined' ? window : global); 3171 | }()); 3172 | --------------------------------------------------------------------------------