diff --git a/app/controllers/pastes_controller.rb b/app/controllers/pastes_controller.rb
index adbc009..07dddfe 100644
--- a/app/controllers/pastes_controller.rb
+++ b/app/controllers/pastes_controller.rb
@@ -33,9 +33,7 @@ def index
@pastes_count = @pastes.count
@pastes_pages = Paginator.new(self, @pastes_count, @limit, params[:page])
@offset ||= @pastes_pages.current.offset
- @pastes = @pastes.all(:order => "#{Paste.table_name}.created_on DESC",
- :offset => @offset,
- :limit => @limit)
+ @pastes = @pastes.all.order(:created_on).offset(@offset).limit(@limit)
respond_to do |format|
format.html { render :layout => false if request.xhr? }
diff --git a/app/helpers/pastes_helper.rb b/app/helpers/pastes_helper.rb
index adca49e..3ed8419 100644
--- a/app/helpers/pastes_helper.rb
+++ b/app/helpers/pastes_helper.rb
@@ -16,38 +16,76 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-require 'coderay'
-
module PastesHelper
- PASTEBIN_LANGS = ["Plain Text", "C", "C++", "Java", "JavaScript",
- "Python", "Ruby", "PHP", "SQL", "XML", "Diff"]
+ PASTEBIN_LANGS = ["Plain Text", "Apache Config", "Bash", "C", "C++", "CSS", "CoffeeScript",
+ "Diff", "Git","Go","HAML", "JSON", "Java", "JavaScript", "Markdown",
+ "Markup", "Nginx","PHP", "Perl", "Python", "Ruby", "SQL", "XML", "YAML"]
- # This maps pretty language/syntax name to identifier that CodeRay
+ # This maps pretty language/syntax name to identifier that PrismJS
# can understand. If a language is not mapped, unchanged name is
# assumed for the scanner.
PASTEBIN_SCANNERS_MAP = {
- "Plain Text" => "Plaintext",
- "C++" => "CPlusPlus"
+ 'Plain Text' => 'Plaintext',
+ 'Markup' => 'markup',
+ 'CSS' => 'css',
+ 'Apache Config' => 'apacheconf',
+ 'Bash' => 'coffeescript',
+ 'CoffeeScript' => 'coffeescript',
+ 'Git' => 'git',
+ 'Go' => 'go',
+ 'HAML' => 'haml',
+ 'JSON' => 'json',
+ 'Markdown' => 'markdown',
+ 'Nginx' => 'nginx',
+ 'Perl' => 'perl',
+ 'C' => 'c',
+ 'Java' => 'java',
+ 'JavaScript' => 'javascript',
+ 'Python' => 'python',
+ 'Ruby' => 'ruby',
+ 'PHP' => 'php',
+ 'SQL' => 'sql',
+ 'XML' => 'xml',
+ 'Diff' => 'diff',
+ 'YAML' => 'yaml',
+ 'C++' => 'cpp'
}
PASTEBIN_MIME_TYPES_MAP = {
- "Plain Text" => "text/plain",
- "C" => "text/x-c",
- "Ruby" => "text/x-ruby",
- "PHP" => "text/x-php",
- "XML" => "application/xml"
+ 'Plain Text' => 'text/plain',
+ 'C' => 'text/x-c',
+ 'Ruby' => 'text/x-ruby',
+ 'PHP' => 'text/x-php',
+ 'XML' => 'application/xml',
+ 'python' => 'text/x-python',
+ 'markdown' => 'text/x-markdown',
+ 'bash' => 'text/x-shellscript',
+ 'coffeescript' => 'application/vnd.coffeescript'
}
PASTEBIN_FILE_SUFFIX_MAP = {
- "Plain Text" => "txt",
- "C++" => "cpp",
- "JavaScript" => "js",
- "Python" => "py",
- "Ruby" => "rb",
+ 'Plain Text' => 'txt',
+ 'Plaintext' => 'txt',
+ 'git' => 'txt',
+ 'markup' => 'txt',
+ 'C++' => 'cpp',
+ 'JavaScript' => 'js',
+ 'Python' => 'py',
+ 'Ruby' => 'rb',
+ 'ruby' => 'rb',
+ 'python' => 'py',
+ 'coffeescript' => 'coffee',
+ 'bash' => 'sh',
+ 'markdown' => 'md',
+ 'perl' => 'pl',
+ 'javascript' => 'js',
+ 'nginx' => 'conf',
+ 'apacheconf' => 'conf'
+
}
PASTEBIN_EXPIRATION_CHOICES = [[:never, 0], [:an_hour, 1.hour], [:a_day, 1.day]]
-
+
def pastebin_lang_to_scanner(lang)
PASTEBIN_SCANNERS_MAP[lang] || lang
end
@@ -77,11 +115,10 @@ def pastebin_mime_type(paste)
end
def highlighted_content_for_paste(paste)
- #Redmine::SyntaxHighlighting.highlight_by_language(paste.text, paste.lang)
-
- # TODO: hard-coding code-ray for :table option
- content_tag :div, :class => "syntaxhl box" do
- ::CodeRay.scan(paste.text, paste.lang).html(:line_numbers => :table).html_safe
+ content_tag :pre do
+ content_tag :code, :class => "language-#{pastebin_lang_to_scanner(paste.lang)} line-numbers" do
+ paste.text
+ end
end
end
@@ -109,7 +146,7 @@ def delete_paste_link(paste, title = l(:button_delete))
link_to_if_authorized(title, url_to_paste("destroy", paste),
:class => "icon icon-del",
:method => :delete,
- :confirm => l(:text_paste_delete_confirmation))
+ data: {confirm: l(:text_paste_delete_confirmation)})
end
def download_paste_link(paste, title = l(:button_download))
diff --git a/app/models/paste.rb b/app/models/paste.rb
index e8bab7c..1508c4a 100644
--- a/app/models/paste.rb
+++ b/app/models/paste.rb
@@ -26,14 +26,21 @@ class Paste < ActiveRecord::Base
belongs_to :project
belongs_to :author, :class_name => 'User'
- scope :for_project, lambda { |project|
+ scope :for_project, -> (project) {
where({ :project_id => project })
}
- scope :secure, where("access_token IS NOT NULL")
+ scope :secure, -> {
+ where("access_token IS NOT NULL")
+ }
+
+ scope :expired, -> {
+ where("expires_at <= current_timestamp")
+ }
- scope :expired, where("expires_at <= current_timestamp")
- scope :unexpired, where("expires_at IS NULL OR expires_at > current_timestamp")
+ scope :unexpired, -> {
+ where("expires_at IS NULL OR expires_at > current_timestamp")
+ }
#
# * Restrict to projects where the user has a role allowing to view
@@ -47,10 +54,10 @@ class Paste < ActiveRecord::Base
#
# * Never show expired pastes even to an admin.
#
- scope :visible, lambda{ |user=nil, options={}|
+ scope :visible, -> (user=nil, options={}) {
user ||= User.current
- s = where(Project.allowed_to_condition(user, :view_pastes, options)).includes(:project)
+ s = where(Project.allowed_to_condition(user, :view_pastes, options)).joins(:project)
unless user.admin?
s = s.where(["access_token IS NULL OR author_id = ?", user.id])
end
@@ -72,22 +79,23 @@ class Paste < ActiveRecord::Base
# conflict by overriding the user.
#
def self.visible_to(user, options={})
- with_exclusive_scope do
+ unscoped do
Paste.visible(user, options)
end
end
acts_as_searchable :columns => ["#{table_name}.title", "#{table_name}.text"],
- :include => :project
+ :scope => preload(:project)
acts_as_event :title => Proc.new{ |o| o.title },
:url => Proc.new{ |o| { :controller => 'pastes', :action => 'show',
:id => o.to_param } }
- acts_as_activity_provider :find_options => {:include => [:project, :author]},
+ acts_as_activity_provider :scope => preload([:project, :author]),
:author_key => :author_id
def title
+ return if new_record?
t = super
t.present? ? t : "Paste ##{id}"
end
@@ -132,7 +140,7 @@ def self.secure_id?(id)
end
def self.find_by_secure_id(id)
- with_exclusive_scope do
+ unscoped do
find_by_access_token(id)
end
end
@@ -146,7 +154,7 @@ def expire_in(seconds)
end
def self.wipe_all_expired
- with_exclusive_scope do
+ unscoped do
Paste.expired.delete_all
end
end
diff --git a/app/views/pastes/show.html.erb b/app/views/pastes/show.html.erb
index 1bf302f..119d11b 100644
--- a/app/views/pastes/show.html.erb
+++ b/app/views/pastes/show.html.erb
@@ -1,16 +1,7 @@
<% content_for :header_tags do %>
<%= stylesheet_link_tag 'scm' %>
-
-
+<%= stylesheet_link_tag 'prism', :plugin => "redmine_pastebin" %>
+<%= javascript_include_tag 'prism', :plugin => "redmine_pastebin" %>
<% end %>
<% html_title @paste.title %>
diff --git a/assets/javascripts/prism.js b/assets/javascripts/prism.js
new file mode 100644
index 0000000..c3e3407
--- /dev/null
+++ b/assets/javascripts/prism.js
@@ -0,0 +1,2264 @@
+/* http://prismjs.com/download.html?themes=prism-coy&languages=markup+css+clike+javascript+apacheconf+bash+c+cpp+coffeescript+ruby+diff+git+go+haml+java+json+markdown+nginx+perl+php+python+sass+sql+yaml&plugins=line-numbers+toolbar+copy-to-clipboard */
+var _self = (typeof window !== 'undefined')
+ ? window // if in browser
+ : (
+ (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope)
+ ? self // if in worker
+ : {} // if in node js
+ );
+
+/**
+ * Prism: Lightweight, robust, elegant syntax highlighting
+ * MIT license http://www.opensource.org/licenses/mit-license.php/
+ * @author Lea Verou http://lea.verou.me
+ */
+
+var Prism = (function(){
+
+// Private helper vars
+var lang = /\blang(?:uage)?-(\w+)\b/i;
+var uniqueId = 0;
+
+var _ = _self.Prism = {
+ manual: _self.Prism && _self.Prism.manual,
+ util: {
+ encode: function (tokens) {
+ if (tokens instanceof Token) {
+ return new Token(tokens.type, _.util.encode(tokens.content), tokens.alias);
+ } else if (_.util.type(tokens) === 'Array') {
+ return tokens.map(_.util.encode);
+ } else {
+ return tokens.replace(/&/g, '&').replace(/ text.length) {
+ // Something went terribly wrong, ABORT, ABORT!
+ return;
+ }
+
+ if (str instanceof Token) {
+ continue;
+ }
+
+ pattern.lastIndex = 0;
+
+ var match = pattern.exec(str),
+ delNum = 1;
+
+ // Greedy patterns can override/remove up to two previously matched tokens
+ if (!match && greedy && i != strarr.length - 1) {
+ pattern.lastIndex = pos;
+ match = pattern.exec(text);
+ if (!match) {
+ break;
+ }
+
+ var from = match.index + (lookbehind ? match[1].length : 0),
+ to = match.index + match[0].length,
+ k = i,
+ p = pos;
+
+ for (var len = strarr.length; k < len && (p < to || (!strarr[k].type && !strarr[k - 1].greedy)); ++k) {
+ p += strarr[k].length;
+ // Move the index i to the element in strarr that is closest to from
+ if (from >= p) {
+ ++i;
+ pos = p;
+ }
+ }
+
+ /*
+ * If strarr[i] is a Token, then the match starts inside another Token, which is invalid
+ * If strarr[k - 1] is greedy we are in conflict with another greedy pattern
+ */
+ if (strarr[i] instanceof Token || strarr[k - 1].greedy) {
+ continue;
+ }
+
+ // Number of tokens to delete and replace with the new match
+ delNum = k - i;
+ str = text.slice(pos, p);
+ match.index -= pos;
+ }
+
+ if (!match) {
+ if (oneshot) {
+ break;
+ }
+
+ continue;
+ }
+
+ if(lookbehind) {
+ lookbehindLength = match[1].length;
+ }
+
+ var from = match.index + lookbehindLength,
+ match = match[0].slice(lookbehindLength),
+ to = from + match.length,
+ before = str.slice(0, from),
+ after = str.slice(to);
+
+ var args = [i, delNum];
+
+ if (before) {
+ ++i;
+ pos += before.length;
+ args.push(before);
+ }
+
+ var wrapped = new Token(token, inside? _.tokenize(match, inside) : match, alias, match, greedy);
+
+ args.push(wrapped);
+
+ if (after) {
+ args.push(after);
+ }
+
+ Array.prototype.splice.apply(strarr, args);
+
+ if (delNum != 1)
+ _.matchGrammar(text, strarr, grammar, i, pos, true, token);
+
+ if (oneshot)
+ break;
+ }
+ }
+ }
+ },
+
+ tokenize: function(text, grammar, language) {
+ var strarr = [text];
+
+ var rest = grammar.rest;
+
+ if (rest) {
+ for (var token in rest) {
+ grammar[token] = rest[token];
+ }
+
+ delete grammar.rest;
+ }
+
+ _.matchGrammar(text, strarr, grammar, 0, 0, false);
+
+ return strarr;
+ },
+
+ hooks: {
+ all: {},
+
+ add: function (name, callback) {
+ var hooks = _.hooks.all;
+
+ hooks[name] = hooks[name] || [];
+
+ hooks[name].push(callback);
+ },
+
+ run: function (name, env) {
+ var callbacks = _.hooks.all[name];
+
+ if (!callbacks || !callbacks.length) {
+ return;
+ }
+
+ for (var i=0, callback; callback = callbacks[i++];) {
+ callback(env);
+ }
+ }
+ }
+};
+
+var Token = _.Token = function(type, content, alias, matchedStr, greedy) {
+ this.type = type;
+ this.content = content;
+ this.alias = alias;
+ // Copy of the full string this token was created from
+ this.length = (matchedStr || "").length|0;
+ this.greedy = !!greedy;
+};
+
+Token.stringify = function(o, language, parent) {
+ if (typeof o == 'string') {
+ return o;
+ }
+
+ if (_.util.type(o) === 'Array') {
+ return o.map(function(element) {
+ return Token.stringify(element, language, o);
+ }).join('');
+ }
+
+ var env = {
+ type: o.type,
+ content: Token.stringify(o.content, language, parent),
+ tag: 'span',
+ classes: ['token', o.type],
+ attributes: {},
+ language: language,
+ parent: parent
+ };
+
+ if (env.type == 'comment') {
+ env.attributes['spellcheck'] = 'true';
+ }
+
+ if (o.alias) {
+ var aliases = _.util.type(o.alias) === 'Array' ? o.alias : [o.alias];
+ Array.prototype.push.apply(env.classes, aliases);
+ }
+
+ _.hooks.run('wrap', env);
+
+ var attributes = Object.keys(env.attributes).map(function(name) {
+ return name + '="' + (env.attributes[name] || '').replace(/"/g, '"') + '"';
+ }).join(' ');
+
+ return '<' + env.tag + ' class="' + env.classes.join(' ') + '"' + (attributes ? ' ' + attributes : '') + '>' + env.content + '' + env.tag + '>';
+
+};
+
+if (!_self.document) {
+ if (!_self.addEventListener) {
+ // in Node.js
+ return _self.Prism;
+ }
+ // In worker
+ _self.addEventListener('message', function(evt) {
+ var message = JSON.parse(evt.data),
+ lang = message.language,
+ code = message.code,
+ immediateClose = message.immediateClose;
+
+ _self.postMessage(_.highlight(code, _.languages[lang], lang));
+ if (immediateClose) {
+ _self.close();
+ }
+ }, false);
+
+ return _self.Prism;
+}
+
+//Get current script and highlight
+var script = document.currentScript || [].slice.call(document.getElementsByTagName("script")).pop();
+
+if (script) {
+ _.filename = script.src;
+
+ if (document.addEventListener && !_.manual && !script.hasAttribute('data-manual')) {
+ if(document.readyState !== "loading") {
+ if (window.requestAnimationFrame) {
+ window.requestAnimationFrame(_.highlightAll);
+ } else {
+ window.setTimeout(_.highlightAll, 16);
+ }
+ }
+ else {
+ document.addEventListener('DOMContentLoaded', _.highlightAll);
+ }
+ }
+}
+
+return _self.Prism;
+
+})();
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Prism;
+}
+
+// hack for components to work correctly in node.js
+if (typeof global !== 'undefined') {
+ global.Prism = Prism;
+}
+;
+Prism.languages.markup = {
+ 'comment': //,
+ 'prolog': /<\?[\s\S]+?\?>/,
+ 'doctype': //i,
+ 'cdata': //i,
+ 'tag': {
+ pattern: /<\/?(?!\d)[^\s>\/=$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\s\S])*\1|[^\s'">=]+))?)*\s*\/?>/i,
+ inside: {
+ 'tag': {
+ pattern: /^<\/?[^\s>\/]+/i,
+ inside: {
+ 'punctuation': /^<\/?/,
+ 'namespace': /^[^\s>\/:]+:/
+ }
+ },
+ 'attr-value': {
+ pattern: /=(?:('|")[\s\S]*?(\1)|[^\s>]+)/i,
+ inside: {
+ 'punctuation': /[=>"']/
+ }
+ },
+ 'punctuation': /\/?>/,
+ 'attr-name': {
+ pattern: /[^\s>\/]+/,
+ inside: {
+ 'namespace': /^[^\s>\/:]+:/
+ }
+ }
+
+ }
+ },
+ 'entity': /?[\da-z]{1,8};/i
+};
+
+Prism.languages.markup['tag'].inside['attr-value'].inside['entity'] =
+ Prism.languages.markup['entity'];
+
+// Plugin to make entity title show the real entity, idea by Roman Komarov
+Prism.hooks.add('wrap', function(env) {
+
+ if (env.type === 'entity') {
+ env.attributes['title'] = env.content.replace(/&/, '&');
+ }
+});
+
+Prism.languages.xml = Prism.languages.markup;
+Prism.languages.html = Prism.languages.markup;
+Prism.languages.mathml = Prism.languages.markup;
+Prism.languages.svg = Prism.languages.markup;
+
+Prism.languages.css = {
+ 'comment': /\/\*[\s\S]*?\*\//,
+ 'atrule': {
+ pattern: /@[\w-]+?.*?(;|(?=\s*\{))/i,
+ inside: {
+ 'rule': /@[\w-]+/
+ // See rest below
+ }
+ },
+ 'url': /url\((?:(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,
+ 'selector': /[^\{\}\s][^\{\};]*?(?=\s*\{)/,
+ 'string': {
+ pattern: /("|')(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,
+ greedy: true
+ },
+ 'property': /(\b|\B)[\w-]+(?=\s*:)/i,
+ 'important': /\B!important\b/i,
+ 'function': /[-a-z0-9]+(?=\()/i,
+ 'punctuation': /[(){};:]/
+};
+
+Prism.languages.css['atrule'].inside.rest = Prism.util.clone(Prism.languages.css);
+
+if (Prism.languages.markup) {
+ Prism.languages.insertBefore('markup', 'tag', {
+ 'style': {
+ pattern: /(