texttube 5.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +22 -0
- data/CHANGES.md +148 -0
- data/Gemfile +24 -0
- data/README.md +306 -0
- data/Rakefile +43 -0
- data/lib/ext/blank.rb +17 -0
- data/lib/ext/to_constant.rb +26 -0
- data/lib/texttube/base.rb +158 -0
- data/lib/texttube/filterable.rb +53 -0
- data/lib/texttube/filters/coderay.rb +68 -0
- data/lib/texttube/filters/embedding_audio.rb +61 -0
- data/lib/texttube/filters/embedding_video.rb +111 -0
- data/lib/texttube/filters/inside_block.rb +35 -0
- data/lib/texttube/filters/link_reffing.rb +144 -0
- data/lib/texttube/version.rb +6 -0
- data/lib/texttube.rb +23 -0
- data/spec/coderay_spec.rb +87 -0
- data/spec/embedding_audio_spec.rb +42 -0
- data/spec/inside_block_spec.rb +35 -0
- data/spec/link_reffing_spec.rb +97 -0
- data/spec/markdownfilters_spec.rb +280 -0
- data/spec/spec_helper.rb +33 -0
- data/texttube.gemspec +20 -0
- metadata +107 -0
@@ -0,0 +1,158 @@
|
|
1
|
+
module TextTube
|
2
|
+
|
3
|
+
# Inherit from this and wrap it around a string and
|
4
|
+
# you can then run filters against it.
|
5
|
+
# @example
|
6
|
+
# module AFilter
|
7
|
+
# extend TextTube::Filterable
|
8
|
+
#
|
9
|
+
# filter_with :double do |text|
|
10
|
+
# text * 2
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# filter_with :triple do |text|
|
14
|
+
# text * 3
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# module BFil
|
19
|
+
# extend TextTube::Filterable
|
20
|
+
#
|
21
|
+
# filter_with :spacial do |current,options|
|
22
|
+
# current.split(//).join " "
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# class NeuS < TextTube::Base
|
27
|
+
# register BFil
|
28
|
+
# register AFilter
|
29
|
+
# register do
|
30
|
+
# filter_with :dashes do |text|
|
31
|
+
# "---#{text}---"
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# n = NeuS.new "abc"
|
37
|
+
# n.filter
|
38
|
+
# # => "---a b ca b ca b ca b ca b ca b c---"
|
39
|
+
# n
|
40
|
+
# # => "abc"
|
41
|
+
class Base < ::String
|
42
|
+
|
43
|
+
# @!attribute [rw] options
|
44
|
+
# This instance's options.
|
45
|
+
# @return [Hash]
|
46
|
+
attr_accessor :options
|
47
|
+
|
48
|
+
# @param [#to_s] text The original text.
|
49
|
+
# @param [Hash] options Options that will be passed on to every instance.
|
50
|
+
# @example
|
51
|
+
# n = NeuS.new "abc"
|
52
|
+
def initialize( text, options={} )
|
53
|
+
@options = options
|
54
|
+
@filters ||= []
|
55
|
+
super text
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
class << self
|
60
|
+
|
61
|
+
# Global options. Every descendant will get these.
|
62
|
+
def options
|
63
|
+
@options ||= {}
|
64
|
+
end
|
65
|
+
|
66
|
+
# remove all filters
|
67
|
+
# @todo remove methods too.
|
68
|
+
def reset!
|
69
|
+
@filters = []
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
# Filters added.
|
74
|
+
def filters
|
75
|
+
@filters ||= []
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
# Register a module of filters.
|
80
|
+
# @example
|
81
|
+
# class MyFilter < TextTube::Base
|
82
|
+
# register TextTube::LinkReffing
|
83
|
+
# register TextTube::Coderay
|
84
|
+
# register do
|
85
|
+
# filter_with :dashes do |text|
|
86
|
+
# "---#{text}---"
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# I could've made it so there's no need to write the
|
92
|
+
# `filter_with` part, but this way your code will be
|
93
|
+
# clearer to those reading it later, and you can add
|
94
|
+
# in any other stuff you want done too, you don't just
|
95
|
+
# have to set up a filter.
|
96
|
+
def register( filter=nil, &block )
|
97
|
+
@filters ||= []
|
98
|
+
if block
|
99
|
+
name = filter
|
100
|
+
|
101
|
+
filter = Module.new do
|
102
|
+
extend Filterable
|
103
|
+
end
|
104
|
+
filter.instance_eval &block
|
105
|
+
end
|
106
|
+
@filters += filter.filters
|
107
|
+
include filter
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
# A clean slate
|
112
|
+
# @private
|
113
|
+
def inherited(subclass)
|
114
|
+
subclass.reset!
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
# Run the filters. If the names of filters are provided as arguments then only those filters will be run, in that order (left first, to right).
|
120
|
+
# @overload filter()
|
121
|
+
# Run all the filters in the order they were registered.
|
122
|
+
# n = NeuS.new "abc"
|
123
|
+
# n.filter
|
124
|
+
# # => "---a b ca b ca b ca b ca b ca b c---"
|
125
|
+
# @overload filter( filters )
|
126
|
+
# Run the given filters in the order given.
|
127
|
+
# @param [Array<#to_sym>] filters
|
128
|
+
# @example
|
129
|
+
# n = NeuS.new "abc"
|
130
|
+
# n.filter :spacial
|
131
|
+
# # => "a b c"
|
132
|
+
# n.filter :spacial, :dashes
|
133
|
+
# # => "---a b c---"
|
134
|
+
# @overload filter( filters_and_options )
|
135
|
+
# Run the given filters in the order given with the given options. Pass any options for a filter at the end, by giving the options as a hash with the name of the filter. I'd prefer to give the filter name and the options at the same time, but the Ruby argument parser can't handle it, which is fair enough :)
|
136
|
+
# @example
|
137
|
+
# n = NeuS.new "abc"
|
138
|
+
# n.filter :dashes, :spacial, :dashes => {some_option_for_dashes: true}
|
139
|
+
def filter( *order_and_options )
|
140
|
+
if order_and_options.last.respond_to?(:keys)
|
141
|
+
*order,options = *order_and_options
|
142
|
+
else
|
143
|
+
order,options = order_and_options, {}
|
144
|
+
end
|
145
|
+
order = order.flatten
|
146
|
+
order = @options.fetch :order, self.class.filters if order.empty?
|
147
|
+
order.inject(self){|current,filter|
|
148
|
+
send filter, current, options
|
149
|
+
}
|
150
|
+
end
|
151
|
+
|
152
|
+
# See all added filters.
|
153
|
+
def filters
|
154
|
+
self.class.filters
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module TextTube
|
2
|
+
|
3
|
+
# Add this to your filter module.
|
4
|
+
# @example
|
5
|
+
# module AFilter
|
6
|
+
# extend TextTube::Filterable
|
7
|
+
#
|
8
|
+
# filter_with :double do |text|
|
9
|
+
# text * 2
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# filter_with :triple do |text|
|
13
|
+
# text * 3
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
module Filterable
|
17
|
+
|
18
|
+
# See all current filters.
|
19
|
+
# @return [Array<Symbol>]
|
20
|
+
def filters
|
21
|
+
@filters ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
# Add a filter.
|
25
|
+
# @example
|
26
|
+
# filter_with :triple do |text|
|
27
|
+
# text * 3
|
28
|
+
# end
|
29
|
+
# filter_with :number do |text,options|
|
30
|
+
# text * options[:times].to_i
|
31
|
+
# end
|
32
|
+
# @param [#to_sym] name
|
33
|
+
# @yield [String, Hash] Pass the string to be filtered, and optionally, any options.
|
34
|
+
def filter_with name, &block
|
35
|
+
name = name.to_sym
|
36
|
+
filters << name unless filters.include? name
|
37
|
+
define_method name do |current=self, options=nil|
|
38
|
+
if current.respond_to? :keys
|
39
|
+
options=current
|
40
|
+
current=self
|
41
|
+
end
|
42
|
+
options = [options, @options, self.class.options].find{|opts|
|
43
|
+
!opts.nil? &&
|
44
|
+
opts.respond_to?(:keys) &&
|
45
|
+
!opts.empty?
|
46
|
+
} || {}
|
47
|
+
|
48
|
+
block.call current, options[name]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end # Filterable
|
53
|
+
end # TextTube
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module TextTube
|
3
|
+
|
4
|
+
require 'nokogiri'
|
5
|
+
require_relative "../../ext/blank.rb"
|
6
|
+
require 'coderay'
|
7
|
+
require_relative "../filterable.rb"
|
8
|
+
|
9
|
+
# a filter for Coderay
|
10
|
+
module Coderay
|
11
|
+
extend Filterable
|
12
|
+
|
13
|
+
filter_with :coderay do |text|
|
14
|
+
TextTube::Coderay.run text
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
# @param [String] content
|
19
|
+
# @param [Hash] options
|
20
|
+
# @return [String]
|
21
|
+
def self.run(content, options={})
|
22
|
+
options = {lang: :ruby } if options.blank?
|
23
|
+
doc = Nokogiri::HTML::fragment(content)
|
24
|
+
|
25
|
+
code_blocks = doc.xpath("pre/code").map do |code_block|
|
26
|
+
#un-escape as Coderay will escape it again
|
27
|
+
inner_html = code_block.inner_html
|
28
|
+
|
29
|
+
# following the convention of Rack::Codehighlighter
|
30
|
+
if inner_html.start_with?("::::")
|
31
|
+
lines = inner_html.split("\n")
|
32
|
+
options[:lang] = lines.shift.match(%r{::::(\w+)})[1].to_sym
|
33
|
+
inner_html = lines.join("\n")
|
34
|
+
end
|
35
|
+
|
36
|
+
if (options[:lang] == :skip) || (! options.has_key? :lang )
|
37
|
+
code_block.inner_html = inner_html
|
38
|
+
else
|
39
|
+
code = Coderay.codify(Coderay.html_unescape(inner_html), options[:lang])
|
40
|
+
code_block.inner_html = code
|
41
|
+
code_block["class"] = "CodeRay"
|
42
|
+
end
|
43
|
+
end#block
|
44
|
+
|
45
|
+
doc.to_s
|
46
|
+
end#def
|
47
|
+
|
48
|
+
# @private
|
49
|
+
# Unescape the HTML as the Coderay scanner won't work otherwise.
|
50
|
+
def self.html_unescape(a_string)
|
51
|
+
a_string.gsub('&', '&').gsub('<', '<').gsub('>',
|
52
|
+
'>').gsub('"', '"')
|
53
|
+
end#def
|
54
|
+
|
55
|
+
# Run the Coderay scanner.
|
56
|
+
# @private
|
57
|
+
# @param [String] str
|
58
|
+
# @param [String] lang
|
59
|
+
# @example
|
60
|
+
# self.class.codify "x = 2", "ruby"
|
61
|
+
def self.codify(str, lang)
|
62
|
+
CodeRay.scan(str, lang).html
|
63
|
+
end#def
|
64
|
+
|
65
|
+
end#class
|
66
|
+
|
67
|
+
|
68
|
+
end#module
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require_relative "../filterable.rb"
|
3
|
+
|
4
|
+
module TextTube
|
5
|
+
|
6
|
+
# Embed some audio via [audio[link|name]]
|
7
|
+
module EmbeddingAudio
|
8
|
+
extend TextTube::Filterable
|
9
|
+
|
10
|
+
filter_with :embeddingaudio do |text|
|
11
|
+
TextTube::EmbeddingAudio.run text
|
12
|
+
end
|
13
|
+
|
14
|
+
# default attributes
|
15
|
+
DEFAULTS = {
|
16
|
+
src_base: "/streams/",
|
17
|
+
preload: "metadata",
|
18
|
+
fallback_text: "Your browser does not support HTML5, update your browser you fool!",
|
19
|
+
controls: "controls",
|
20
|
+
}
|
21
|
+
|
22
|
+
# [audio[link|name]]
|
23
|
+
R_link = / # [audio[url|description]]
|
24
|
+
\[audio\[ # opening square brackets
|
25
|
+
([^\|]+) # link
|
26
|
+
\| # separator
|
27
|
+
([^\[]+) # description
|
28
|
+
\]\] # closing square brackets
|
29
|
+
/x
|
30
|
+
|
31
|
+
|
32
|
+
# @param [String] content
|
33
|
+
# @param [Hash] options
|
34
|
+
# @return [String]
|
35
|
+
def self.run(content, options={})
|
36
|
+
options ||= {}
|
37
|
+
attributes = DEFAULTS.merge options
|
38
|
+
|
39
|
+
content.gsub( R_link ) { |m|
|
40
|
+
url,desc = $1,$2
|
41
|
+
EmbeddingAudio::render_tag(url,desc,attributes)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
# Does the grunt work of rendering the tag.
|
47
|
+
# @private
|
48
|
+
# @param [String] link
|
49
|
+
# @param [String] desc
|
50
|
+
# @param [Hash] attributes
|
51
|
+
def self.render_tag(link,desc,attributes)
|
52
|
+
fallback_text = attributes.delete(:fallback_text)
|
53
|
+
src_base = attributes.delete(:src_base)
|
54
|
+
make_inner = ->(lnk){%Q!<source src='#{src_base}#{lnk}' type='audio/#{File.extname(lnk)[1..-1]}' />!}
|
55
|
+
inner = make_inner.( link )
|
56
|
+
inner += make_inner.( link.sub(/m4a$/, "ogg") ) if File.extname(link) == ".m4a"
|
57
|
+
%Q!<div class='audio'><h3>#{desc}</h3><audio #{attributes.map{|(k,v)| "#{k}='#{v}'" }.join(" ")}>#{inner}#{fallback_text}</audio></div>!.strip.gsub /\s+/, " "
|
58
|
+
end
|
59
|
+
|
60
|
+
end # class
|
61
|
+
end # module
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require_relative "../filterable.rb"
|
3
|
+
|
4
|
+
module TextTube
|
5
|
+
|
6
|
+
# Embed video via [embed_SIZE[url|description]]
|
7
|
+
module EmbeddingVideo
|
8
|
+
extend TextTube::Filterable
|
9
|
+
|
10
|
+
filter_with :embeddingvideo do |text|
|
11
|
+
TextTube::EmbeddingVideo.run text
|
12
|
+
end
|
13
|
+
|
14
|
+
# List of available sites.
|
15
|
+
SITES = {
|
16
|
+
# "video.google".to_sym => ['http://www.video.google.com/',->(w,h,url){ %Q!! }],
|
17
|
+
# :brightcove => ['http://www.brightcove.com/',->(w,h,url){ %Q!! }],
|
18
|
+
# :photobucket => ['http://www.photobucket.com/',->(w,h,url){ %Q!! }],
|
19
|
+
:youtube => {
|
20
|
+
name: 'http://www.youtube.com/',
|
21
|
+
html: ->(w,h,url){ %Q!<iframe title="YouTube video player" class="youtube-player" type="text/html" width="#{w}" height="#{h}" src="#{url}" frameborder="0"></iframe>!.strip},
|
22
|
+
url_morph: ->(orig){ orig.sub( %r{watch\?v=}, 'embed/') }
|
23
|
+
},
|
24
|
+
# :dailymotion => ['http://dailymotion.com/',->(w,h,url){ %Q!! }],
|
25
|
+
# :ifilm => ['http://ifilm.com/',->(w,h,url){ %Q!! }],
|
26
|
+
# :break => ['http://break.com/',->(w,h,url){ %Q!! }],
|
27
|
+
# :blip => ['http://blip.tv/',->(w,h,url){ %Q!! }],
|
28
|
+
# :grindtv => ['http://www.grindtv.com/',->(w,h,url){ %Q!! }],
|
29
|
+
# :metacafe => ['http://metacafe.com/',->(w,h,url){ %Q!! }],
|
30
|
+
# :myspace => ['http://vids.myspace.com/',->(w,h,url){ %Q!! }],
|
31
|
+
# :vimeo => ['http://vimeo.com/',->(w,h,url){ %Q!! }],
|
32
|
+
# :buzznet => ['http://buzznet.com/',->(w,h,url){ %Q!! }],
|
33
|
+
# :liveleak => ['http://www.liveleak.com/',->(w,h,url){ %Q!! }],
|
34
|
+
# :stupidvideos => ['http://stupidvideos.com/',->(w,h,url){ %Q!! }],
|
35
|
+
# :flixya => ['http://www.flixya.com/',->(w,h,url){ %Q!! }],
|
36
|
+
# :gofish => ['http://gofish.com/',->(w,h,url){ %Q!! }],
|
37
|
+
# :kewego => ['http://kewego.com/',->(w,h,url){ %Q!! }],
|
38
|
+
# :lulu => ['http://lulu.tv/',->(w,h,url){ %Q!! }],
|
39
|
+
# :pandora => ['http://pandora.tv/',->(w,h,url){ %Q!! }],
|
40
|
+
# :viddler => ['http://www.viddler.com/',->(w,h,url){ %Q!! }],
|
41
|
+
# :myheavy => ['http://myheavy.com/',->(w,h,url){ %Q!! }],
|
42
|
+
# :putfile => ['http://putfile.com/',->(w,h,url){ %Q!! }],
|
43
|
+
# :stupidvideos => ['http://stupidvideos.com/',->(w,h,url){ %Q!! }],
|
44
|
+
# :vmix => ['http://vmix.com/',->(w,h,url){ %Q!! }],
|
45
|
+
# :zippyvideos => ['http://zippyvideos.com/',->(w,h,url){ %Q!! }],
|
46
|
+
# :castpost => ['http://castpost.com/',->(w,h,url){ %Q!! }],
|
47
|
+
# :dotv => ['http://dotv.com/',->(w,h,url){ %Q!! }],
|
48
|
+
# :famster => ['http://famster.com/',->(w,h,url){ %Q!! }],
|
49
|
+
# :gawkk => ['http://gawkk.com/',->(w,h,url){ %Q!! }],
|
50
|
+
# :tubetorial => ['http://tubetorial.com/',->(w,h,url){ %Q!! }],
|
51
|
+
# :MeraVideo => ['http://MeraVideo.com/',->(w,h,url){ %Q!! }],
|
52
|
+
# :Porkolt => ['http://Porkolt.com/',->(w,h,url){ %Q!! }],
|
53
|
+
# :VideoWebTown => ['http://VideoWebTown.com/',->(w,h,url){ %Q!! }],
|
54
|
+
# :Vidmax => ['http://Vidmax.com/',->(w,h,url){ %Q!! }],
|
55
|
+
# :clipmoon => ['http://www.clipmoon.com/',->(w,h,url){ %Q!! }],
|
56
|
+
# :motorsportmad => ['http://motorsportmad.com/',->(w,h,url){ %Q!! }],
|
57
|
+
# :thatshow => ['http://www.thatshow.com/',->(w,h,url){ %Q!! }],
|
58
|
+
# :clipchef => ['http://clipchef.com/',->(w,h,url){ %Q!! }],
|
59
|
+
}
|
60
|
+
|
61
|
+
# Some standard player sizes.
|
62
|
+
SIZES = {
|
63
|
+
small: [560,345],
|
64
|
+
medium: [640,390],
|
65
|
+
large: [853,510],
|
66
|
+
largest: [1280,750] }
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
# r_url = %r{
|
71
|
+
# http(?:s)?\:// #http https ://
|
72
|
+
# (?:(?:www|vids)\.)? # www. vids.
|
73
|
+
# (.+?) # domain name (hopefully)
|
74
|
+
# \.(?:com|tv) # .com .tv
|
75
|
+
# /? # optional trailing slash
|
76
|
+
# }x
|
77
|
+
|
78
|
+
# Pattern for deconstructing [embed_SIZE[url|description]]
|
79
|
+
R_link = / # [embed_SIZE[url|description]]
|
80
|
+
\[embed\_?([a-z]+)?\[ # opening square brackets
|
81
|
+
(\S+) # link
|
82
|
+
\| # separator
|
83
|
+
([^\[]+) # description
|
84
|
+
\]\] # closing square brackets
|
85
|
+
/x
|
86
|
+
|
87
|
+
|
88
|
+
# @param [String] content
|
89
|
+
# @param [Hash] options
|
90
|
+
# @return [String]
|
91
|
+
def self.run(content, options={})
|
92
|
+
options ||= {}
|
93
|
+
content.gsub( R_link ) { |m|
|
94
|
+
size,url,desc = $1,$2,$3
|
95
|
+
|
96
|
+
unless size.nil?
|
97
|
+
res = SIZES[size.to_sym] || SIZES[:medium] #resolution
|
98
|
+
else
|
99
|
+
res = SIZES[:medium]
|
100
|
+
end
|
101
|
+
|
102
|
+
#"res: #{res.inspect} size: #{size}, url:#{url}, desc:#{desc}"
|
103
|
+
|
104
|
+
emb_url = SITES[:youtube][:url_morph].(url)
|
105
|
+
SITES[:youtube][:html].(res.first, res.last, emb_url )
|
106
|
+
}
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end # class
|
111
|
+
end # module
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'nokogiri'
|
3
|
+
require_relative "../filterable.rb"
|
4
|
+
|
5
|
+
module TextTube
|
6
|
+
|
7
|
+
# This finds html tags with "markdown='1'" as an attribute, runs markdown over the contents, then removes the markdown attribute, allowing markdown within html blocks
|
8
|
+
module InsideBlock
|
9
|
+
extend TextTube::Filterable
|
10
|
+
|
11
|
+
filter_with :insideblock do |text|
|
12
|
+
TextTube::InsideBlock.run text
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param [String] content
|
16
|
+
# @param [Hash] options
|
17
|
+
# @option options [Constant] The markdown parser to use. I'm not sure this bit really works for other parsers than RDiscount.
|
18
|
+
def self.run( content, options={})
|
19
|
+
options ||= {}
|
20
|
+
if options[:markdown_parser].nil?
|
21
|
+
require 'rdiscount'
|
22
|
+
markdown_parser=RDiscount
|
23
|
+
end
|
24
|
+
doc = Nokogiri::HTML::fragment(content)
|
25
|
+
|
26
|
+
(doc/"*[@markdown='1']").each do |ele|
|
27
|
+
ele.inner_html = markdown_parser.new(ele.inner_html).to_html
|
28
|
+
ele.remove_attribute("markdown")
|
29
|
+
end
|
30
|
+
|
31
|
+
doc.to_s
|
32
|
+
end # run
|
33
|
+
|
34
|
+
end # class
|
35
|
+
end # module
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require_relative "../filterable.rb"
|
3
|
+
|
4
|
+
module TextTube
|
5
|
+
|
6
|
+
# @author Iain Barnett
|
7
|
+
# A class to take links in the format `[[link|description]]` and give them a number reference, then output them in markdown format. Note: this is not the same as reference links in markdown, this is more akin to the way books will refer to references or footnotes with a number.
|
8
|
+
#P.S. I don't like to make functions private, we're all adults, so to use this call Link_reffing#run, #format_links is for internal use.
|
9
|
+
module LinkReffing
|
10
|
+
extend Filterable
|
11
|
+
|
12
|
+
filter_with :linkreffing do |text, options|
|
13
|
+
TextTube::LinkReffing.run text, options
|
14
|
+
end
|
15
|
+
|
16
|
+
# These are the html codes for superscript 0 - 9
|
17
|
+
UNITS = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'] #unicode superscript numbers
|
18
|
+
|
19
|
+
# Matches [[link|description]]
|
20
|
+
Pattern = /
|
21
|
+
\[\[ # opening square brackets
|
22
|
+
(?<link>\S+)
|
23
|
+
\s*\|\s* # separator
|
24
|
+
(?<description>[^\[]+)
|
25
|
+
\]\] # closing square brackets
|
26
|
+
/x
|
27
|
+
|
28
|
+
# a lambda function to transform a link and a number into a markdown reference link.
|
29
|
+
# @param [String] lnk The url.
|
30
|
+
# @param [String] num The reference number.
|
31
|
+
Reffer = ->(lnk, num){ %Q![#{lnk}](##{num} "Jump to reference")!}
|
32
|
+
|
33
|
+
# A lambda to transform a link and a number to a HTML reference link.
|
34
|
+
RefHTMLer = ->(lnk, num){ %Q!<a href="##{num}" title="Jump to reference">#{lnk}</a>! }
|
35
|
+
|
36
|
+
# A lambda to transform a href and a description into an HTML link.
|
37
|
+
HTMLer = ->(lnk, desc){ %Q! <a href="#{lnk}">#{desc}</a>! }
|
38
|
+
|
39
|
+
# A lambda to transform a link and a description into an inline Markdown link.
|
40
|
+
Markdowner = ->(lnk, desc){ %Q! [#{desc}](#{lnk})! }
|
41
|
+
|
42
|
+
# Noner = ->(_,_) { "" } # this isn't needed but will sit here as a reminder.
|
43
|
+
|
44
|
+
|
45
|
+
# Takes markdown content with ref links and turns it into 100% markdown.
|
46
|
+
# @param [String] content The markdown content with links to ref.
|
47
|
+
# @option options [#to_s] :format The format of the link you want added. The options are :html, :markdown. The default is :markdown
|
48
|
+
# @option options [#to_s] :kind The kind of link you want added. The options are :reference, :inline, :none. The default is :reference
|
49
|
+
# @option options [String,nil] :div_id ID of the div to wrap reference links in. Defaults to "reflinks". Set to nil or false for no div.
|
50
|
+
# @return [String] The string formatted as markdown e.g. `[http://cheat.errtheblog.com/s/yard/more/and/m...](http://cheat.errtheblog.com/s/yard/more/and/more/and/more/ "http://cheat.errtheblog.com/s/yard/more/and/more/and/more/")`
|
51
|
+
def self.run(content, options={})
|
52
|
+
return content if content.blank?
|
53
|
+
text = content.dup
|
54
|
+
options ||= {}
|
55
|
+
kind = options.fetch :kind, :reference
|
56
|
+
format = options.fetch( :format, :markdown )
|
57
|
+
formatter = if kind == :inline
|
58
|
+
if format == :html
|
59
|
+
HTMLer
|
60
|
+
else
|
61
|
+
Markdowner
|
62
|
+
end
|
63
|
+
elsif kind == :none
|
64
|
+
nil # none is needed
|
65
|
+
else # kind == :reference
|
66
|
+
if format == :html
|
67
|
+
RefHTMLer
|
68
|
+
else
|
69
|
+
Reffer
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
div_id = options.has_key?(:div_id) ?
|
74
|
+
options[:div_id] :
|
75
|
+
:reflinks
|
76
|
+
|
77
|
+
cur = 0 #current number
|
78
|
+
|
79
|
+
# if there are no reflinks found
|
80
|
+
# this will remain false
|
81
|
+
# and `divit` won't be run.
|
82
|
+
has_reflinks = false
|
83
|
+
|
84
|
+
links = [] #to store the matches
|
85
|
+
|
86
|
+
text.gsub! Pattern do |md| #block to pass to gsub
|
87
|
+
has_reflinks = true
|
88
|
+
if kind == :inline
|
89
|
+
formatter.($1,$2)
|
90
|
+
elsif kind == :none
|
91
|
+
""
|
92
|
+
else # kind == :reference
|
93
|
+
mags = cur.divmod(10) #get magnitude of number
|
94
|
+
ref_tag = mags.first >= 1 ?
|
95
|
+
UNITS[mags.first] :
|
96
|
+
'' #sort out tens
|
97
|
+
|
98
|
+
ref_tag += UNITS[mags.last] #units
|
99
|
+
retval = formatter.(ref_tag,cur)
|
100
|
+
|
101
|
+
links << [$1, $2, cur] # add to the words list
|
102
|
+
cur += 1 #increase current number
|
103
|
+
retval
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
if !links.empty?
|
108
|
+
if has_reflinks && div_id
|
109
|
+
"#{text}\n#{LinkReffing.divit( div_id ) { format_links(links) }}"
|
110
|
+
else
|
111
|
+
"#{text}\n#{format_links(links)}"
|
112
|
+
end
|
113
|
+
else
|
114
|
+
text
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
# This func outputs the link as valid markdown.
|
120
|
+
# @param [Array<String,String,Integer>] links A list of 2-length arrays containing the url and the description and the reference number.
|
121
|
+
def self.format_links( links )
|
122
|
+
links.map{ |(link, description, cur)|
|
123
|
+
display_link = link.length >= 45 ?
|
124
|
+
link[0,45] + "..." :
|
125
|
+
link
|
126
|
+
%Q!\n<a name="#{cur}"></a>#{LeftSq}#{cur}#{RightSq} [#{display_link}](#{link} "#{link}") #{description}\n\n!
|
127
|
+
}.join
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
# Wraps things in a div. If no id given, no div.
|
132
|
+
# @param [#to_s] id The ID attribute for the div.
|
133
|
+
def self.divit( id )
|
134
|
+
"<div markdown='1' id='#{id}'>#{ yield }</div>"
|
135
|
+
end
|
136
|
+
|
137
|
+
# HTML code for [
|
138
|
+
LeftSq = "["
|
139
|
+
# HTML code for ]
|
140
|
+
RightSq = "]"
|
141
|
+
|
142
|
+
end#class
|
143
|
+
|
144
|
+
end#module
|
data/lib/texttube.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require_relative "./ext/blank.rb"
|
4
|
+
require_relative "./ext/to_constant.rb"
|
5
|
+
|
6
|
+
# The original. Maybe. I haven't checked.
|
7
|
+
module TextTube
|
8
|
+
|
9
|
+
# Require all the filters.
|
10
|
+
# The `map` is there to show the result of this and
|
11
|
+
# show which libs were required (if so desired).
|
12
|
+
# @return [Array<String,TrueClass>]
|
13
|
+
def self.load_all_filters
|
14
|
+
Dir.glob( File.join File.dirname(__FILE__), "/texttube/filters/*.rb" )
|
15
|
+
.reject{|name| name.end_with? "version.rb" }
|
16
|
+
.map{|filter|
|
17
|
+
tf = require filter
|
18
|
+
[File.basename(filter, ".rb").gsub("_",""), tf]
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
end
|