Permalink
Browse files

Implemented rudimentary document history information in output.

Use with:

    $ markout -h path/to/file.txt
    $ markout --history path/to/file.txt

There must be a .git directory with valid Git repo in the same directory as the document, as of now.

NOTES:

* Clean up mess with defaults in API
* History should be default
*
  • Loading branch information...
1 parent 5632be5 commit ad03781b784209f8522993033e7a326446989a5a @karmi committed Apr 3, 2009
View
@@ -21,6 +21,12 @@ OptionParser.new do |o|
opts[:format] = f
end
+ o.on("-h",
+ "--history",
+ "Output document history based on Git repository info") do |h|
+ opts[:history] = !h.nil?
+ end
+
o.on("-t",
"--template [TEMPLATE]",
"Output template") do |t|
@@ -34,15 +40,15 @@ OptionParser.new do |o|
opts[:output] = d
end
- o.on("-h", "--help", "Show help") do |h|
+ o.on("--help", "Show help") do |h|
puts o
exit
end
end.parse!
die("You need to provide path to a Markdown formatted text file!
USAGE: #{$0} [options] [path/to/file]
- Type #{$0} -h for help") if ARGV.empty?
+ Type #{$0} --help for help") if ARGV.empty?
begin
time = Time.now
View
@@ -1,6 +1,6 @@
$LOAD_PATH << File.join( File.dirname(__FILE__), 'markout' )
-%w{output document formatter}.each { |lib| require lib }
+%w{output document formatter history revision}.each { |lib| require lib }
module Markout
View
@@ -5,14 +5,17 @@ module Markout
class Document
- attr_reader :path, :base_path, :content
+ attr_reader :path, :base_path, :filename, :content, :history
- def initialize(path)
+ def initialize(path, options={ :history => true })
@path = File.expand_path(path)
@base_path = Pathname.new( File.dirname(@path) )
+ @filename = File.basename(path)
+ @options = options
raise FileNotFound, "File #{@path} not found" unless File.exist?(@path)
raise FileNotSupported, "File #{@path} is not plain text" unless MIME::Types.type_for(@path).any? { |type| type.media_type == 'text' }
@content = File.read(path)
+ @history = Markout::History.new(self, :git_dir => @options[:git_dir]) if @options[:history]
end
end
@@ -34,11 +34,24 @@ def print_style
File.read template_path.join('print.css')
end
+ def syntax_highlighter
+ s = ''
+ s << '<script type="text/javascript">' + File.read( template_path.join('..', 'common', 'code_highlighter.js') ) + '</script>'
+ s << '<script type="text/javascript">' + File.read( template_path.join('..', 'common', 'syntax_diff.js') ) + '</script>'
+ s << '<style type="text/css" media="screen">' + File.read( template_path.join('..', 'common', 'code.css') ) + '</style>'
+ end
+
def title
h1 = content.match(/<h1\s*.*>(.+)<\/h1>/)[1] rescue nil
h1 || ''
end
+ def history
+ @document.history
+ end
+
+ private
+
# TODO : Make 'alt' attribute optional
# TODO : Cleanup?
def suck_in_images!
@@ -0,0 +1,11 @@
+.diff .ins {
+ color: green;
+/* background-color: rgba(0, 0, 0, 0.03);*/
+ background-color: #f0f5f1;
+}
+
+.diff .del {
+ color: red;
+/* background-color: rgba(0, 0, 0, 0.03);*/
+ background-color: #f0f5f1;
+}
@@ -0,0 +1,188 @@
+/* Unobtrustive Code Highlighter By Dan Webb 11/2005
+ Version: 0.4
+
+ Usage:
+ Add a script tag for this script and any stylesets you need to use
+ to the page in question, add correct class names to CODE elements,
+ define CSS styles for elements. That's it!
+
+ Known to work on:
+ IE 5.5+ PC
+ Firefox/Mozilla PC/Mac
+ Opera 7.23 + PC
+ Safari 2
+
+ Known to degrade gracefully on:
+ IE5.0 PC
+
+ Note: IE5.0 fails due to the use of lookahead in some stylesets. To avoid script errors
+ in older browsers use expressions that use lookahead in string format when defining stylesets.
+
+ This script is inspired by star-light by entirely cunning Dean Edwards
+ http://dean.edwards.name/star-light/.
+*/
+
+// replace callback support for safari.
+if ("a".replace(/a/, function() {return "b"}) != "b") (function(){
+ var default_replace = String.prototype.replace;
+ String.prototype.replace = function(search,replace){
+ // replace is not function
+ if(typeof replace != "function"){
+ return default_replace.apply(this,arguments)
+ }
+ var str = "" + this;
+ var callback = replace;
+ // search string is not RegExp
+ if(!(search instanceof RegExp)){
+ var idx = str.indexOf(search);
+ return (
+ idx == -1 ? str :
+ default_replace.apply(str,[search,callback(search, idx, str)])
+ )
+ }
+ var reg = search;
+ var result = [];
+ var lastidx = reg.lastIndex;
+ var re;
+ while((re = reg.exec(str)) != null){
+ var idx = re.index;
+ var args = re.concat(idx, str);
+ result.push(
+ str.slice(lastidx,idx),
+ callback.apply(null,args).toString()
+ );
+ if(!reg.global){
+ lastidx += RegExp.lastMatch.length;
+ break
+ }else{
+ lastidx = reg.lastIndex;
+ }
+ }
+ result.push(str.slice(lastidx));
+ return result.join("")
+ }
+})();
+
+var CodeHighlighter = { styleSets : new Array };
+
+CodeHighlighter.addStyle = function(name, rules) {
+ // using push test to disallow older browsers from adding styleSets
+ if ([].push) this.styleSets.push({
+ name : name,
+ rules : rules,
+ ignoreCase : arguments[2] || false
+ })
+
+ function setEvent() {
+ // set highlighter to run on load (use LowPro if present)
+ if (typeof Event != 'undefined' && typeof Event.onReady == 'function')
+ return Event.onReady(CodeHighlighter.init.bind(CodeHighlighter));
+
+ var old = window.onload;
+
+ if (typeof window.onload != 'function') {
+ window.onload = function() { CodeHighlighter.init() };
+ } else {
+ window.onload = function() {
+ old();
+ CodeHighlighter.init();
+ }
+ }
+ }
+
+ // only set the event when the first style is added
+ if (this.styleSets.length==1) setEvent();
+}
+
+CodeHighlighter.init = function() {
+ if (!document.getElementsByTagName) return;
+ if ("a".replace(/a/, function() {return "b"}) != "b") return; // throw out Safari versions that don't support replace function
+ // throw out older browsers
+
+ var codeEls = document.getElementsByTagName("CODE");
+ // collect array of all pre elements
+ codeEls.filter = function(f) {
+ var a = new Array;
+ for (var i = 0; i < this.length; i++) if (f(this[i])) a[a.length] = this[i];
+ return a;
+ }
+
+ var rules = new Array;
+ rules.toString = function() {
+ // joins regexes into one big parallel regex
+ var exps = new Array;
+ for (var i = 0; i < this.length; i++) exps.push(this[i].exp);
+ return exps.join("|");
+ }
+
+ function addRule(className, rule) {
+ // add a replace rule
+ var exp = (typeof rule.exp != "string")?String(rule.exp).substr(1, String(rule.exp).length-2):rule.exp;
+ // converts regex rules to strings and chops of the slashes
+ rules.push({
+ className : className,
+ exp : "(" + exp + ")",
+ length : (exp.match(/(^|[^\\])\([^?]/g) || "").length + 1, // number of subexps in rule
+ replacement : rule.replacement || null
+ });
+ }
+
+ function parse(text, ignoreCase) {
+ // main text parsing and replacement
+ return text.replace(new RegExp(rules, (ignoreCase)?"gi":"g"), function() {
+ var i = 0, j = 1, rule;
+ while (rule = rules[i++]) {
+ if (arguments[j]) {
+ // if no custom replacement defined do the simple replacement
+ if (!rule.replacement) return "<span class=\"" + rule.className + "\">" + arguments[0] + "</span>";
+ else {
+ // replace $0 with the className then do normal replaces
+ var str = rule.replacement.replace("$0", rule.className);
+ for (var k = 1; k <= rule.length - 1; k++) str = str.replace("$" + k, arguments[j + k]);
+ return str;
+ }
+ } else j+= rule.length;
+ }
+ });
+ }
+
+ function highlightCode(styleSet) {
+ // clear rules array
+ var parsed, clsRx = new RegExp("(\\s|^)" + styleSet.name + "(\\s|$)");
+ rules.length = 0;
+
+ // get stylable elements by filtering out all code elements without the correct className
+ var stylableEls = codeEls.filter(function(item) { return clsRx.test(item.className) });
+
+ // add style rules to parser
+ for (var className in styleSet.rules) addRule(className, styleSet.rules[className]);
+
+
+ // replace for all elements
+ for (var i = 0; i < stylableEls.length; i++) {
+ // EVIL hack to fix IE whitespace badness if it's inside a <pre>
+ if (/MSIE/.test(navigator.appVersion) && stylableEls[i].parentNode.nodeName == 'PRE') {
+ stylableEls[i] = stylableEls[i].parentNode;
+
+ parsed = stylableEls[i].innerHTML.replace(/(<code[^>]*>)([^<]*)<\/code>/i, function() {
+ return arguments[1] + parse(arguments[2], styleSet.ignoreCase) + "</code>"
+ });
+ parsed = parsed.replace(/\n( *)/g, function() {
+ var spaces = "";
+ for (var i = 0; i < arguments[1].length; i++) spaces+= "&nbsp;";
+ return "\n" + spaces;
+ });
+ parsed = parsed.replace(/\t/g, "&nbsp;&nbsp;&nbsp;&nbsp;");
+ parsed = parsed.replace(/\n(<\/\w+>)?/g, "<br />$1").replace(/<br \/>[\n\r\s]*<br \/>/g, "<p><br></p>");
+
+ } else parsed = parse(stylableEls[i].innerHTML, styleSet.ignoreCase);
+
+ stylableEls[i].innerHTML = parsed;
+ }
+ }
+
+ // run highlighter on all stylesets
+ for (var i=0; i < this.styleSets.length; i++) {
+ highlightCode(this.styleSets[i]);
+ }
+}
@@ -0,0 +1,8 @@
+CodeHighlighter.addStyle("diff",{
+ ins : {
+ exp : /\+[^\n]+/
+ },
+ del : {
+ exp : /\-[^\n]+/
+ }
+});
@@ -15,13 +15,31 @@
<%= print_style %>
</style>
+ <%= syntax_highlighter %>
+
</head>
<body>
<div id="content">
<%= content %>
- </div>
+ </div><!-- /#content -->
+
+ <% if history %>
+ <div id="history">
+ <h2>History</h2>
+ <% history.revisions.each do |revision| %>
+ <div class="revision">
+ <h4><%= revision.date %> : <%= revision.subject %> (<%= revision.author %>)</h4>
+ <pre>
+<code class="diff">
+<%= revision.diff %>
+</code>
+ </pre>
+ </div><!-- /.revision -->
+ <% end %>
+ </div><!-- /#history -->
+ <% end %>
</body>
</html>
View
@@ -6,8 +6,8 @@ class Output
def initialize(path, options = {})
@path = path
- @document = Document.new(@path)
@options = options
+ @document = Document.new(@path, :history => @options[:history])
@format = @options[:format] || 'html'
@formatter = pick_formatter.new( @document, :template => @options[:template] || 'default' )
@output = ''
@@ -26,7 +26,7 @@ def test_should_read_file_contents
end
def new_valid_document
- @document = Document.new( fixture_file('markdown.txt') )
+ @document = Document.new( fixture_file('markdown.txt'), :history => false )
end
end
@@ -5,7 +5,7 @@ module Markout
class FormatterTest < Test::Unit::TestCase
def test_should_raise_exception
- @formatter = Formatter.new( Document.new( fixture_file('markdown.txt') ) )
+ @formatter = Formatter.new( Document.new( fixture_file('markdown.txt'), :history => false ) )
assert_raise(NoMethodError) { @formatter.export }
end
@@ -6,7 +6,7 @@ module Markout
class HtmlTest < Test::Unit::TestCase
def setup
- @document = Document.new( fixture_file('markdown.txt') )
+ @document = Document.new( fixture_file('markdown.txt'), :history => false )
end
def test_should_set_default_template
@@ -14,15 +14,15 @@ def test_default_initialization
def test_export
default_output
- assert_equal fixture('markdown.html'), @output.export
+ assert_equal fixture('markdown.html'), @output.export, "Exported text does not match output"
end
def test_export_to_file
# TODO
end
def default_output
- @output = Output.new( fixture_file('markdown.txt') )
+ @output = Output.new( fixture_file('markdown.txt'), :history => false )
end
end

0 comments on commit ad03781

Please sign in to comment.