Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7a2479f85 | |||
| a8b230a490 | |||
| 041b01f171 | |||
| 1c475f3215 | |||
| ab699d94b0 | |||
| 942d32c9b6 | |||
| 97f15f0b18 | |||
| ed49358c50 | |||
| 85eeecd140 | |||
| 30fd40fbe5 | |||
| 2ed262cacd | |||
| 29a1ef8f8a | |||
| 772d18ee62 | |||
| dd604d9942 | |||
| 85abc83427 | |||
| 6d8220629c | |||
| 6ff7ada096 | |||
| 8575049de5 | |||
| bb6fb0c253 | |||
| eb2ad9f840 | |||
| d0dd23fc11 | |||
| 7ae4acbdb0 | |||
| 881590ab37 | |||
| 5e479dc5d9 | |||
| 7db9c2e762 | |||
| 3767a11d21 |
@@ -497,9 +497,13 @@ like Rack::Auth, OmniAuth, etc.
|
|||||||
Precious::App.set(:wiki_options, {:universal_toc => false})
|
Precious::App.set(:wiki_options, {:universal_toc => false})
|
||||||
run Precious::App
|
run Precious::App
|
||||||
|
|
||||||
## Windows Filename Validation
|
## WINDOWS FILENAME VALIDATION
|
||||||
Note that filenames on windows must not contain any of the following characters `\ / : * ? " < > |`. See [this support article](http://support.microsoft.com/kb/177506) for details.
|
Note that filenames on windows must not contain any of the following characters `\ / : * ? " < > |`. See [this support article](http://support.microsoft.com/kb/177506) for details.
|
||||||
|
|
||||||
|
## LIB.SO ERROR
|
||||||
|
|
||||||
|
`Could not open library 'lib.so'` may be solved by installing `python-devel` on Fedora or `python-dev` on Ubuntu.
|
||||||
|
|
||||||
## CONTRIBUTE
|
## CONTRIBUTE
|
||||||
|
|
||||||
If you'd like to hack on Gollum, start by forking my repo on GitHub:
|
If you'd like to hack on Gollum, start by forking my repo on GitHub:
|
||||||
@@ -528,8 +532,8 @@ your changes merged back into core is as follows:
|
|||||||
$ gem push gollum-X.Y.Z.gem
|
$ gem push gollum-X.Y.Z.gem
|
||||||
|
|
||||||
## BUILDING THE GEM FROM MASTER
|
## BUILDING THE GEM FROM MASTER
|
||||||
$ gem uninstall -aix gollum
|
$ gem uninstall -aIx gollum
|
||||||
$ git clone https://github.com/github/gollum.git
|
$ git clone https://github.com/github/gollum.git
|
||||||
$ cd gollum
|
$ cd gollum
|
||||||
gollum$ rake build
|
gollum$ rake build
|
||||||
gollum$ gem install pkg/gollum*.gem
|
gollum$ gem install --no-ri --no-rdoc pkg/gollum*.gem
|
||||||
|
|||||||
+3
-3
@@ -5,8 +5,8 @@ Gem::Specification.new do |s|
|
|||||||
s.required_ruby_version = ">= 1.8.7"
|
s.required_ruby_version = ">= 1.8.7"
|
||||||
|
|
||||||
s.name = 'gollum'
|
s.name = 'gollum'
|
||||||
s.version = '2.1.7'
|
s.version = '2.2.1'
|
||||||
s.date = '2012-08-25'
|
s.date = '2012-09-02'
|
||||||
s.rubyforge_project = 'gollum'
|
s.rubyforge_project = 'gollum'
|
||||||
|
|
||||||
s.summary = "A simple, Git-powered wiki."
|
s.summary = "A simple, Git-powered wiki."
|
||||||
@@ -27,7 +27,7 @@ Gem::Specification.new do |s|
|
|||||||
s.add_dependency('github-markup', [">= 0.7.0", "< 1.0.0"])
|
s.add_dependency('github-markup', [">= 0.7.0", "< 1.0.0"])
|
||||||
s.add_dependency('github-markdown')
|
s.add_dependency('github-markdown')
|
||||||
s.add_dependency('pygments.rb', "~> 0.2.0")
|
s.add_dependency('pygments.rb', "~> 0.2.0")
|
||||||
s.add_dependency('posix-spawn', "~> 0.3.0")
|
s.add_dependency('escape_utils', "0.2.4")
|
||||||
s.add_dependency('sinatra', "~> 1.0")
|
s.add_dependency('sinatra', "~> 1.0")
|
||||||
s.add_dependency('mustache', [">= 0.11.2", "< 1.0.0"])
|
s.add_dependency('mustache', [">= 0.11.2", "< 1.0.0"])
|
||||||
s.add_dependency('sanitize', "~> 2.0.0")
|
s.add_dependency('sanitize', "~> 2.0.0")
|
||||||
|
|||||||
+2
-1
@@ -20,9 +20,10 @@ require File.expand_path('../gollum/markup', __FILE__)
|
|||||||
require File.expand_path('../gollum/sanitization', __FILE__)
|
require File.expand_path('../gollum/sanitization', __FILE__)
|
||||||
require File.expand_path('../gollum/tex', __FILE__)
|
require File.expand_path('../gollum/tex', __FILE__)
|
||||||
require File.expand_path('../gollum/web_sequence_diagram', __FILE__)
|
require File.expand_path('../gollum/web_sequence_diagram', __FILE__)
|
||||||
|
require File.expand_path('../gollum/frontend/uri_encode_component', __FILE__)
|
||||||
|
|
||||||
module Gollum
|
module Gollum
|
||||||
VERSION = '2.1.7'
|
VERSION = '2.2.1'
|
||||||
|
|
||||||
def self.assets_path
|
def self.assets_path
|
||||||
::File.expand_path('gollum/frontend/public', ::File.dirname(__FILE__))
|
::File.expand_path('gollum/frontend/public', ::File.dirname(__FILE__))
|
||||||
|
|||||||
@@ -100,9 +100,13 @@ module Gollum
|
|||||||
|
|
||||||
tree.blobs.each do |blob|
|
tree.blobs.each do |blob|
|
||||||
next if page_path_scheduled_for_deletion?(index.tree, fullpath)
|
next if page_path_scheduled_for_deletion?(index.tree, fullpath)
|
||||||
file = blob.name.downcase.sub(/\.\w+$/, '')
|
|
||||||
file_ext = ::File.extname(blob.name).sub(/^\./, '')
|
existing_file = blob.name.downcase.sub(/\.\w+$/, '')
|
||||||
if downpath == file && !(allow_same_ext && file_ext == ext)
|
existing_file_ext = ::File.extname(blob.name).sub(/^\./, '')
|
||||||
|
|
||||||
|
new_file_ext = ::File.extname(path).sub(/^\./, '')
|
||||||
|
|
||||||
|
if downpath == existing_file && !(allow_same_ext && new_file_ext == existing_file_ext)
|
||||||
raise DuplicatePageError.new(dir, blob.name, path)
|
raise DuplicatePageError.new(dir, blob.name, path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ require 'gollum/frontend/views/layout'
|
|||||||
require 'gollum/frontend/views/editable'
|
require 'gollum/frontend/views/editable'
|
||||||
require 'gollum/frontend/views/has_page'
|
require 'gollum/frontend/views/has_page'
|
||||||
|
|
||||||
require File.expand_path '../uri_encode_component', __FILE__
|
|
||||||
require File.expand_path '../helpers', __FILE__
|
require File.expand_path '../helpers', __FILE__
|
||||||
|
|
||||||
# Fix to_url
|
# Fix to_url
|
||||||
@@ -283,12 +282,6 @@ module Precious
|
|||||||
mustache :compare
|
mustache :compare
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/_tex.png' do
|
|
||||||
content_type 'image/png'
|
|
||||||
formula = Base64.decode64(params[:data])
|
|
||||||
Gollum::Tex.render_formula(formula)
|
|
||||||
end
|
|
||||||
|
|
||||||
get %r{^/(javascript|css|images)} do
|
get %r{^/(javascript|css|images)} do
|
||||||
halt 404
|
halt 404
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -196,7 +196,9 @@
|
|||||||
LanguageDefinition.getHookFunctionFor("activate")();
|
LanguageDefinition.getHookFunctionFor("activate")();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hotkey( cmd ) {
|
function hotkey( e, cmd ) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
var def = LanguageDefinition.getDefinitionFor( cmd );
|
var def = LanguageDefinition.getDefinitionFor( cmd );
|
||||||
if ( typeof def == 'object' ) {
|
if ( typeof def == 'object' ) {
|
||||||
FunctionBar.executeAction( def );
|
FunctionBar.executeAction( def );
|
||||||
@@ -205,8 +207,17 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Mousetrap.bind(['command+b', 'ctrl+b'], function(){ hotkey('function-bold'); });
|
Mousetrap.bind(['command+1', 'ctrl+1'], function( e ){ hotkey( e, 'function-h1' ); });
|
||||||
Mousetrap.bind(['command+i', 'ctrl+i'], function(){ hotkey('function-italic'); });
|
Mousetrap.bind(['command+2', 'ctrl+2'], function( e ){ hotkey( e, 'function-h2' ); });
|
||||||
|
Mousetrap.bind(['command+3', 'ctrl+3'], function( e ){ hotkey( e, 'function-h3' ); });
|
||||||
|
Mousetrap.bind(['command+b', 'ctrl+b'], function( e ){ hotkey( e, 'function-bold' ); });
|
||||||
|
Mousetrap.bind(['command+i', 'ctrl+i'], function( e ){ hotkey( e, 'function-italic' ); });
|
||||||
|
Mousetrap.bind(['command+s', 'ctrl+s'], function( e ){
|
||||||
|
e.preventDefault();
|
||||||
|
$("#gollum-editor-submit").trigger("click");
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
} );
|
} );
|
||||||
} else {
|
} else {
|
||||||
LanguageDefinition._ACTIVE_LANG = name;
|
LanguageDefinition._ACTIVE_LANG = name;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<a id='toggle' class='edit' href='javascript:void(0)' onclick='jsm.toggleLeftRight();'><img src='images/lr_24.png' alt='Toggle left to right' title='Toggle left to right'></a>
|
<a id='toggle' class='edit' href='javascript:void(0)' onclick='jsm.toggleLeftRight();'><img src='images/lr_24.png' alt='Toggle left to right' title='Toggle left to right'></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='editor_bg'></div>
|
<div id='editor_bg' class='editor_bg'></div>
|
||||||
<div class='toolpanel_bg'></div>
|
<div class='toolpanel_bg'></div>
|
||||||
|
|
||||||
<div id='commenttoolpanel' class='toolpanel edit' style='width: 500px; right: 0px; '>
|
<div id='commenttoolpanel' class='toolpanel edit' style='width: 500px; right: 0px; '>
|
||||||
|
|||||||
@@ -62,9 +62,10 @@ initAce( commentEditor, commentEditorSession );
|
|||||||
var baseUrl = location.pathname.split('/').slice(0,-2).join('/');
|
var baseUrl = location.pathname.split('/').slice(0,-2).join('/');
|
||||||
|
|
||||||
// RegExp from http://stackoverflow.com/questions/901115/get-query-string-values-in-javascript
|
// RegExp from http://stackoverflow.com/questions/901115/get-query-string-values-in-javascript
|
||||||
|
// Returns value on success and undefined on failure.
|
||||||
$.key = function( key ) {
|
$.key = function( key ) {
|
||||||
var value = new RegExp( '[\\?&]' + key + '=([^&#]*)' ).exec( location.href );
|
var value = new RegExp( '[\\?&]' + key + '=([^&#]*)' ).exec( location.href );
|
||||||
return ( !value ) ? 0 : value[ 1 ] || 0;
|
return ( !value ) ? undefined : value[ 1 ] || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// True if &create=true
|
// True if &create=true
|
||||||
@@ -73,10 +74,6 @@ var create = $.key( 'create' );
|
|||||||
var pageName = $.key( 'page' );
|
var pageName = $.key( 'page' );
|
||||||
var pathName = $.key( 'path' );
|
var pathName = $.key( 'path' );
|
||||||
|
|
||||||
if ( pathName === 0 ) {
|
|
||||||
pathName = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultCommitMessage = function() {
|
defaultCommitMessage = function() {
|
||||||
var msg = pageName + ' (markdown)';
|
var msg = pageName + ' (markdown)';
|
||||||
|
|
||||||
@@ -100,7 +97,7 @@ $.save = function( commitMessage ) {
|
|||||||
var newLocation = baseUrl;
|
var newLocation = baseUrl;
|
||||||
|
|
||||||
function clean( str ) {
|
function clean( str ) {
|
||||||
return str.replace(/^\/+/g, '/');
|
return str.replace(/^\/+/, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'a%2Fb' => a/b
|
// 'a%2Fb' => a/b
|
||||||
@@ -341,10 +338,23 @@ var applyTimeout = function () {
|
|||||||
/* Load markdown from /data/page into the ace editor.
|
/* Load markdown from /data/page into the ace editor.
|
||||||
~-1 == false; !~-1 == true;
|
~-1 == false; !~-1 == true;
|
||||||
*/
|
*/
|
||||||
if ( !~location.host.indexOf('github.com') ) {
|
if ( !~ location.host.indexOf( 'github.com' ) ) {
|
||||||
|
|
||||||
|
// returns unescaped key with leading slashes removed
|
||||||
|
function key_no_leading_slash( key ) {
|
||||||
|
return unescape( $.key( key ) || '' ).replace( /^\/+/, '' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure leading / is removed from path and that it ends with /
|
||||||
|
var path = key_no_leading_slash( 'path' );
|
||||||
|
// don't append '/' if path is empty from removing leading slash
|
||||||
|
if ( path !== '' && path.charAt( path.length - 1 ) !== '/' ) {
|
||||||
|
path += '/';
|
||||||
|
}
|
||||||
|
|
||||||
jQuery.ajax( {
|
jQuery.ajax( {
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
url: baseUrl + '/data/' + $.key( 'page' ),
|
url: baseUrl + '/data/' + path + key_no_leading_slash( 'page' ),
|
||||||
success: function( data ) {
|
success: function( data ) {
|
||||||
editorSession.setValue( data );
|
editorSession.setValue( data );
|
||||||
}
|
}
|
||||||
@@ -455,6 +465,11 @@ var applyTimeout = function () {
|
|||||||
|
|
||||||
win.jsm.resize = resize;
|
win.jsm.resize = resize;
|
||||||
|
|
||||||
|
// remove editor_bg after loading because
|
||||||
|
// it'll cause problems if toggle left right is used
|
||||||
|
var ebg = doc.getElementById('editor_bg');
|
||||||
|
ebg.parentNode.removeChild(ebg);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Resize can be called an absurd amount of times
|
Resize can be called an absurd amount of times
|
||||||
and will crash the page without debouncing.
|
and will crash the page without debouncing.
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
<script>
|
||||||
|
Mousetrap.bind(['e'], function( e ) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.location = "/edit" + window.location.pathname;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<div id="wiki-wrapper" class="page">
|
<div id="wiki-wrapper" class="page">
|
||||||
<div id="head">
|
<div id="head">
|
||||||
<h1>{{title}}</h1>
|
<h1>{{title}}</h1>
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ module Precious
|
|||||||
|
|
||||||
attr_reader :page, :content
|
attr_reader :page, :content
|
||||||
|
|
||||||
|
# return path set in app.rb not @page.path
|
||||||
|
def path
|
||||||
|
@path
|
||||||
|
end
|
||||||
|
|
||||||
def title
|
def title
|
||||||
"#{@page.title}"
|
"#{@page.title}"
|
||||||
end
|
end
|
||||||
|
|||||||
+8
-18
@@ -155,23 +155,7 @@ module Gollum
|
|||||||
def process_tex(data)
|
def process_tex(data)
|
||||||
@texmap.each do |id, spec|
|
@texmap.each do |id, spec|
|
||||||
type, tex = *spec
|
type, tex = *spec
|
||||||
|
data.gsub!(id, Gollum::Tex.to_html(tex, type))
|
||||||
# Obtain the formula with parameters
|
|
||||||
out = nil
|
|
||||||
begin
|
|
||||||
width, height, align, base64 = Gollum::Tex.render_formula(tex, true)
|
|
||||||
|
|
||||||
# TODO: Should we load the binary inside the html?
|
|
||||||
#out = %{<img width="#{width}" height="#{height}" style="vertical-align: #{align}px;" src="data:image/png;base64,\n#{base64}" alt="#{CGI.escapeHTML(tex)}" />}
|
|
||||||
|
|
||||||
# Use the alignment values from the formula rendering but still use the call to '_tex.png'. Although it will call render_formula()
|
|
||||||
# again, it will use the already cached formula and it might have some advantages from the point of view of browser caching (really not sure here).
|
|
||||||
out = %{<img width="#{width}" height="#{height}" style="vertical-align: #{align}px;" src="#{::File.join(@wiki.base_path, '_tex.png')}?type=#{type}&data=#{Base64.encode64(tex).chomp}" alt="#{CGI.escapeHTML(tex)}" />}
|
|
||||||
rescue # In case of error
|
|
||||||
out = CGI.escapeHTML(tex)
|
|
||||||
end
|
|
||||||
|
|
||||||
data.gsub!(id, out)
|
|
||||||
end
|
end
|
||||||
data
|
data
|
||||||
end
|
end
|
||||||
@@ -591,12 +575,18 @@ module Gollum
|
|||||||
#########################################################################
|
#########################################################################
|
||||||
|
|
||||||
# Extract metadata for data and build metadata table. Metadata
|
# Extract metadata for data and build metadata table. Metadata
|
||||||
# is content found between `<!-- ---` and `-->` markers, and must
|
# is content found between markers, and must
|
||||||
# be a valid YAML mapping.
|
# be a valid YAML mapping.
|
||||||
#
|
#
|
||||||
|
# Because ri and ruby 1.8.7 are awesome, the markers can't
|
||||||
|
# be included in this documentation without triggering
|
||||||
|
# `Unhandled special: Special: type=17`
|
||||||
|
# Please read the source code for the exact markers
|
||||||
|
#
|
||||||
# Returns the String of formatted data with metadata removed.
|
# Returns the String of formatted data with metadata removed.
|
||||||
def extract_metadata(data)
|
def extract_metadata(data)
|
||||||
@metadata ||= {}
|
@metadata ||= {}
|
||||||
|
# The markers are `<!-- ---` and `-->`
|
||||||
data.gsub(/\<\!--+\s+---(.*?)--+\>/m) do
|
data.gsub(/\<\!--+\s+---(.*?)--+\>/m) do
|
||||||
yaml = @wiki.sanitizer.clean($1)
|
yaml = @wiki.sanitizer.clean($1)
|
||||||
hash = YAML.load(yaml)
|
hash = YAML.load(yaml)
|
||||||
|
|||||||
+7
-359
@@ -1,366 +1,14 @@
|
|||||||
require 'fileutils'
|
require 'escape_utils'
|
||||||
require 'shellwords'
|
|
||||||
require 'tmpdir'
|
|
||||||
require 'posix/spawn'
|
|
||||||
require 'base64'
|
|
||||||
|
|
||||||
module Gollum
|
module Gollum
|
||||||
module Tex
|
module Tex
|
||||||
class Error < StandardError; end
|
TEX_URL = "http://www.mathtran.org/cgi-bin/toy/"
|
||||||
|
TEX_SIZES = { :inline => 2, :block => 4 }
|
||||||
|
|
||||||
extend POSIX::Spawn
|
def self.to_html(tex, type = :inline)
|
||||||
|
tex_uri = EscapeUtils.escape_uri(tex)
|
||||||
Template = <<-EOS
|
tex_alt = EscapeUtils.escape_html(tex)
|
||||||
\\documentclass[11pt]{article}
|
%{<img src="#{TEX_URL}?D=#{TEX_SIZES[type]};tex=#{tex_uri}" alt="#{tex_alt}">}
|
||||||
\\pagestyle{empty}
|
|
||||||
\\setlength{\\topskip}{0pt}
|
|
||||||
\\setlength{\\parindent}{0pt}
|
|
||||||
\\setlength{\\abovedisplayskip}{0pt}
|
|
||||||
\\setlength{\\belowdisplayskip}{0pt}
|
|
||||||
|
|
||||||
\\usepackage{geometry}
|
|
||||||
|
|
||||||
\\usepackage{amsfonts}
|
|
||||||
\\usepackage{amsmath}
|
|
||||||
|
|
||||||
\\newsavebox{\\snippetbox}
|
|
||||||
\\newlength{\\snippetwidth}
|
|
||||||
\\newlength{\\snippetheight}
|
|
||||||
\\newlength{\\snippetdepth}
|
|
||||||
\\newlength{\\pagewidth}
|
|
||||||
\\newlength{\\pageheight}
|
|
||||||
\\newlength{\\pagemargin}
|
|
||||||
|
|
||||||
\\begin{lrbox}{\\snippetbox}%
|
|
||||||
\$%s\$
|
|
||||||
\\end{lrbox}
|
|
||||||
|
|
||||||
\\settowidth{\\snippetwidth}{\\usebox{\\snippetbox}}
|
|
||||||
\\settoheight{\\snippetheight}{\\usebox{\\snippetbox}}
|
|
||||||
\\settodepth{\\snippetdepth}{\\usebox{\\snippetbox}}
|
|
||||||
|
|
||||||
\\setlength\\pagemargin{4pt}
|
|
||||||
|
|
||||||
\\setlength\\pagewidth\\snippetwidth
|
|
||||||
\\addtolength\\pagewidth\\pagemargin
|
|
||||||
\\addtolength\\pagewidth\\pagemargin
|
|
||||||
|
|
||||||
\\setlength\\pageheight\\snippetheight
|
|
||||||
\\addtolength{\\pageheight}{\\snippetdepth}
|
|
||||||
\\addtolength\\pageheight\\pagemargin
|
|
||||||
\\addtolength\\pageheight\\pagemargin
|
|
||||||
|
|
||||||
\\newwrite\\foo
|
|
||||||
\\immediate\\openout\\foo=\\jobname.dimensions
|
|
||||||
\\immediate\\write\\foo{snippetdepth = \\the\\snippetdepth}
|
|
||||||
\\immediate\\write\\foo{snippetheight = \\the\\snippetheight}
|
|
||||||
\\immediate\\write\\foo{snippetwidth = \\the\\snippetwidth}
|
|
||||||
\\immediate\\write\\foo{pagewidth = \\the\\pagewidth}
|
|
||||||
\\immediate\\write\\foo{pageheight = \\the\\pageheight}
|
|
||||||
\\immediate\\write\\foo{pagemargin = \\the\\pagemargin}
|
|
||||||
\\closeout\\foo
|
|
||||||
|
|
||||||
\\geometry{paperwidth=\\pagewidth,paperheight=\\pageheight,margin=\\pagemargin}
|
|
||||||
|
|
||||||
\\begin{document}%
|
|
||||||
\\usebox{\\snippetbox}%
|
|
||||||
\\end{document}
|
|
||||||
EOS
|
|
||||||
|
|
||||||
class << self
|
|
||||||
attr_accessor :latex_path
|
|
||||||
end
|
end
|
||||||
|
|
||||||
self.latex_path = 'pdflatex'
|
|
||||||
|
|
||||||
def self.check_dependencies!
|
|
||||||
return if @dependencies_available
|
|
||||||
|
|
||||||
if `which pdflatex` == ""
|
|
||||||
raise Error, "`pdflatex` command not found"
|
|
||||||
end
|
|
||||||
|
|
||||||
if `which gs` == ""
|
|
||||||
raise Error, "`gs` command not found"
|
|
||||||
end
|
|
||||||
|
|
||||||
if `which pnmcrop` == ""
|
|
||||||
raise Error, "`pnmcrop` command not found"
|
|
||||||
end
|
|
||||||
|
|
||||||
if `which pnmpad` == ""
|
|
||||||
raise Error, "`pnmpad` command not found"
|
|
||||||
end
|
|
||||||
|
|
||||||
if `which pnmscale` == ""
|
|
||||||
raise Error, "`pnmscale` command not found"
|
|
||||||
end
|
|
||||||
|
|
||||||
if `which ppmtopgm` == ""
|
|
||||||
raise Error, "`ppmtopgm` command not found"
|
|
||||||
end
|
|
||||||
|
|
||||||
if `which pnmgamma` == ""
|
|
||||||
raise Error, "`pnmgamma` command not found"
|
|
||||||
end
|
|
||||||
|
|
||||||
if `which pnmtopng` == ""
|
|
||||||
raise Error, "`pnmtopng` command not found"
|
|
||||||
end
|
|
||||||
|
|
||||||
@dependencies_available = true
|
|
||||||
end
|
|
||||||
|
|
||||||
# Render the formula and calculate the correct alignment
|
|
||||||
# for the image in the html.
|
|
||||||
#
|
|
||||||
# This is a ruby implementation of the Perl version described
|
|
||||||
# at http://tex.stackexchange.com/questions/44486/pixel-perfect-vertical-alignment-of-image-rendered-tex-snippets
|
|
||||||
#
|
|
||||||
# The main caveat is that rendering takes quite a bit of processing power,
|
|
||||||
# which can make the page load slowly if it has to render each time.
|
|
||||||
# For this reason, the method caches the rendered formula in `/tmp` for reduced
|
|
||||||
# loading time in subsequent loads.
|
|
||||||
#
|
|
||||||
# @param formula the tex formula to render
|
|
||||||
# @param with_properties, if true it returns an array with a base64
|
|
||||||
# string with the image, and the alignment values for the image.
|
|
||||||
# Otherwise it returns the binary image.
|
|
||||||
def self.render_formula(formula, with_properties=false)
|
|
||||||
check_dependencies!
|
|
||||||
|
|
||||||
render_antialias_bits = 4
|
|
||||||
render_oversample = 4
|
|
||||||
display_oversample = 4
|
|
||||||
gamma = 0.3
|
|
||||||
if !with_properties
|
|
||||||
display_oversample = 1
|
|
||||||
gamma = 0.5
|
|
||||||
end
|
|
||||||
|
|
||||||
oversample = render_oversample * display_oversample
|
|
||||||
render_dpi = 96*1.2 * 72.27/72 * oversample # This is 1850.112 dpi.
|
|
||||||
|
|
||||||
|
|
||||||
# Cache rendered formula and returned cached version if it exists
|
|
||||||
|
|
||||||
# First look for the .cache directory in the home folder
|
|
||||||
cache_dir = ::File.expand_path("~/.cache")
|
|
||||||
if not ::File.exists?(cache_dir) or not ::File.directory?(cache_dir)
|
|
||||||
::Dir.mkdir(cache_dir)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Check that the gollum directory exists inside the cache dir
|
|
||||||
cache_dir = ::File.join(cache_dir, "gollum")
|
|
||||||
if not ::File.exists?(cache_dir) or not ::File.directory?(cache_dir)
|
|
||||||
::Dir.mkdir(cache_dir)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Check for the formula in the cache dir
|
|
||||||
hash = Digest::SHA1.hexdigest(formula)
|
|
||||||
cache_file = ::File.join(cache_dir, "tex-#{hash}")
|
|
||||||
|
|
||||||
if ::File.exists?(cache_file)
|
|
||||||
width, height, align, base64 = ::File.open(cache_file, 'rb') { |io| io.read }.split(",")
|
|
||||||
|
|
||||||
if with_properties
|
|
||||||
return width, height, align, base64
|
|
||||||
else
|
|
||||||
return Base64.decode64(base64)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Dir.mktmpdir('tex') do |path|
|
|
||||||
file = ::File.join(path, "formula")
|
|
||||||
|
|
||||||
# --- Write TeX source and compile to PDF.Write snippet into template
|
|
||||||
::File.open(file + ".tex", 'w') { |f| f.write(Template % formula) }
|
|
||||||
|
|
||||||
result = sh_chdir path, "pdflatex",
|
|
||||||
"-halt-on-error",
|
|
||||||
"-output-directory=#{path}",
|
|
||||||
"-output-format=pdf",
|
|
||||||
"#{file}.tex",
|
|
||||||
">#{file}.err 2>&1"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# --- Convert PDF to PNM using Ghostscript.
|
|
||||||
sh "gs",
|
|
||||||
"-q -dNOPAUSE -dBATCH",
|
|
||||||
"-dTextAlphaBits=#{render_antialias_bits}",
|
|
||||||
"-dGraphicsAlphaBits=#{render_antialias_bits}",
|
|
||||||
"-r#{render_dpi}",
|
|
||||||
"-sDEVICE=pnmraw",
|
|
||||||
"-sOutputFile=#{file}.pnm",
|
|
||||||
"#{file}.pdf"
|
|
||||||
|
|
||||||
|
|
||||||
img_width, img_height = pnm_width_height(file + ".pnm")
|
|
||||||
|
|
||||||
|
|
||||||
# --- Read dimensions file written by TeX during processing.
|
|
||||||
#
|
|
||||||
# Example of file contents:
|
|
||||||
# snippetdepth = 6.50009pt
|
|
||||||
# snippetheight = 13.53899pt
|
|
||||||
# snippetwidth = 145.4777pt
|
|
||||||
# pagewidth = 153.4777pt
|
|
||||||
# pageheight = 28.03908pt
|
|
||||||
# pagemargin = 4.0pt
|
|
||||||
dimensions = {}
|
|
||||||
::File.open(file + ".dimensions").readlines.each_with_index do |line, i|
|
|
||||||
if line =~ /^(\S+)\s+=\s+(-?[0-9\.]+)pt$/
|
|
||||||
dimensions[$1] = Float($2) / 72.27 * render_dpi
|
|
||||||
else
|
|
||||||
raise Error, "#{file}.dimensions: invalid line: #{i}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# --- Crop bottom, then measure how much was cropped.
|
|
||||||
sh "pnmcrop -white -bottom #{file}.pnm >#{file}.bottomcrop.pnm"
|
|
||||||
#raise Error, "`pnmcrop` command failed: #{result}" unless ::File.exist?(file + ".bottomcrop.pnm")
|
|
||||||
|
|
||||||
img_width_bottomcrop, img_height_bottomcrop = pnm_width_height("#{file}.bottomcrop.pnm")
|
|
||||||
bottomcrop = img_height - img_height_bottomcrop
|
|
||||||
|
|
||||||
# --- Crop top and sides, then measure how much was cropped from the top.
|
|
||||||
sh "pnmcrop -white #{file}.bottomcrop.pnm > #{file}.crop.pnm"
|
|
||||||
#raise Error, "`pnmcrop` command failed: #{result}" unless ::File.exist?(file + ".crop.pnm")
|
|
||||||
|
|
||||||
cropped_img_width, cropped_img_height = pnm_width_height("#{file}.crop.pnm")
|
|
||||||
topcrop = img_height_bottomcrop - cropped_img_height
|
|
||||||
|
|
||||||
# --- Pad image with specific values on all four sides, in preparation for
|
|
||||||
# downsampling.
|
|
||||||
|
|
||||||
# Calculate bottom padding.
|
|
||||||
snippet_depth = Integer(dimensions["snippetdepth"] + dimensions["pagemargin"] + 0.5) - bottomcrop
|
|
||||||
padded_snippet_depth = round_up(snippet_depth, oversample)
|
|
||||||
increase_snippet_depth = padded_snippet_depth - snippet_depth
|
|
||||||
bottom_padding = increase_snippet_depth
|
|
||||||
|
|
||||||
# --- Next calculate top padding, which depends on bottom padding.
|
|
||||||
|
|
||||||
padded_img_height = round_up(cropped_img_height + bottom_padding,
|
|
||||||
oversample)
|
|
||||||
top_padding = padded_img_height - (cropped_img_height + bottom_padding)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Calculate left and right side padding. Distribute padding evenly.
|
|
||||||
|
|
||||||
padded_img_width = round_up(cropped_img_width, oversample)
|
|
||||||
left_padding = Integer((padded_img_width - cropped_img_width) / 2.0)
|
|
||||||
right_padding = (padded_img_width - cropped_img_width) - left_padding
|
|
||||||
|
|
||||||
|
|
||||||
# --- Pad the final image.
|
|
||||||
result = sh "pnmpad",
|
|
||||||
"-white",
|
|
||||||
"-bottom=#{bottom_padding}",
|
|
||||||
"-top=#{top_padding}",
|
|
||||||
"-left=#{left_padding}",
|
|
||||||
"-right=#{right_padding}",
|
|
||||||
"#{file}.crop.pnm",
|
|
||||||
">#{file}.pad.pnm"
|
|
||||||
|
|
||||||
# --- Sanity check of final size.
|
|
||||||
final_pnm_width, final_pnm_height = pnm_width_height(file + ".pad.pnm")
|
|
||||||
raise Error, "#{final_pnm_width} is not a multiple of #{oversample}" unless final_pnm_width % oversample == 0
|
|
||||||
|
|
||||||
raise "#{final_pnm_height} is not a multiple of #{oversample}" unless final_pnm_height % oversample == 0
|
|
||||||
|
|
||||||
# --- Convert PNM to PNG.
|
|
||||||
|
|
||||||
final_png_width = final_pnm_width / render_oversample
|
|
||||||
final_png_height = final_pnm_height / render_oversample
|
|
||||||
|
|
||||||
result = sh "cat #{file}.pad.pnm",
|
|
||||||
"| ppmtopgm",
|
|
||||||
"| pnmscale -reduce #{render_oversample}",
|
|
||||||
"| pnmgamma #{gamma}",
|
|
||||||
"| pnmtopng -compression 9",
|
|
||||||
"> #{file}.png"
|
|
||||||
|
|
||||||
raise Error, "Conversion to png failed: #{result}" unless ::File.exist?(file + ".png")
|
|
||||||
|
|
||||||
# Calculate html properties
|
|
||||||
html_img_width = final_png_width / display_oversample
|
|
||||||
html_img_height = final_png_height / display_oversample
|
|
||||||
html_img_vertical_align = sprintf("%.0f", -padded_snippet_depth / oversample)
|
|
||||||
png_data_base64 = Base64.encode64(::File.open("#{file}.png") { |io| io.read }).chomp
|
|
||||||
|
|
||||||
::File.open(cache_file, 'w') { |f| f.write(%{#{html_img_width},#{html_img_height},#{html_img_vertical_align},#{png_data_base64}}) }
|
|
||||||
if with_properties
|
|
||||||
return html_img_width, html_img_height, html_img_vertical_align, png_data_base64
|
|
||||||
else
|
|
||||||
::File.read(file + ".png")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def self.sh_chdir(path, *args)
|
|
||||||
origcommand = args * " "
|
|
||||||
return if origcommand == ""
|
|
||||||
|
|
||||||
command = origcommand
|
|
||||||
command.gsub! /(["\\])/, "\\$1"
|
|
||||||
command = %{/bin/sh -c "(#{command}) 2>&1"}
|
|
||||||
|
|
||||||
pid = spawn command, :chdir => path
|
|
||||||
|
|
||||||
result = Process::waitpid(pid)
|
|
||||||
exit_value = Integer($? >> 8), signal_num = Integer($? & 127), dumped_core = Integer($? & 128)
|
|
||||||
raise Error, "Failed #{result}: #{origcommand}. Exit value = #{exit_value}. Signal Num = #{signal_num}. Dumped core = #{dumped_core}" unless $?.success?
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.sh(*args)
|
|
||||||
origcommand = args * " "
|
|
||||||
return if origcommand == ""
|
|
||||||
|
|
||||||
command = origcommand
|
|
||||||
command.gsub! /(["\\])/, "\\$1"
|
|
||||||
command = %{/bin/sh -c "(#{command}) 2>&1"}
|
|
||||||
|
|
||||||
pid = spawn command
|
|
||||||
#pid = spawn *args
|
|
||||||
result = Process::waitpid(pid)
|
|
||||||
exit_value = $? >> 8, signal_num = $? & 127, dumped_core = $? & 128
|
|
||||||
raise Error, "Failed #{result}: #{origcommand}. Exit value = #{exit_value}. Signal Num = #{signal_num}. Dumped core = #{dumped_core}" unless $?.success?
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.round_up(num, mod)
|
|
||||||
num + (num % mod == 0 ? 0 : (mod - (num % mod)))
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.pnm_width_height(filename)
|
|
||||||
raise Error, "#{filename} is not a .pnm file" if filename !~ /\.pnm$/
|
|
||||||
|
|
||||||
width = nil, height = nil
|
|
||||||
::File.open(filename) do |file|
|
|
||||||
# Read first line
|
|
||||||
line = file.gets
|
|
||||||
begin
|
|
||||||
line = file.gets # Read next line, skipping comments
|
|
||||||
end while line && line =~ /^#/
|
|
||||||
|
|
||||||
if line =~ /^(\d+)\s+(\d+)$/
|
|
||||||
width = Integer($1)
|
|
||||||
height = Integer($2)
|
|
||||||
else
|
|
||||||
raise Error, "#{filename}: couldn't read image size"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
raise Error, "#{filename}: couldn't read image size" unless width && height
|
|
||||||
|
|
||||||
return width, height
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+2
-2
@@ -670,13 +670,13 @@ end
|
|||||||
|
|
||||||
test "TeX block syntax" do
|
test "TeX block syntax" do
|
||||||
content = 'a \[ a^2 \] b'
|
content = 'a \[ a^2 \] b'
|
||||||
output = "<p>a<imgwidth=\"15\"height=\"16\"style=\"vertical-align:-1px;\"src=\"/_tex.png?type=block&data=YV4y\"alt=\"a^2\"/>b</p>"
|
output = "<p>a<imgsrc=\"http://www.mathtran.org/cgi-bin/toy/?D=4;tex=a^2\"alt=\"a^2\">b</p>"
|
||||||
compare(content, output, 'md')
|
compare(content, output, 'md')
|
||||||
end
|
end
|
||||||
|
|
||||||
test "TeX inline syntax" do
|
test "TeX inline syntax" do
|
||||||
content = 'a \( a^2 \) b'
|
content = 'a \( a^2 \) b'
|
||||||
output = "<p>a<imgwidth=\"15\"height=\"16\"style=\"vertical-align:-1px;\"src=\"/_tex.png?type=inline&data=YV4y\"alt=\"a^2\"/>b</p>"
|
output = "<p>a<imgsrc=\"http://www.mathtran.org/cgi-bin/toy/?D=2;tex=a^2\"alt=\"a^2\">b</p>"
|
||||||
compare(content, output, 'md')
|
compare(content, output, 'md')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user