serif 0.0.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +3 -0
- data/Gemfile.lock +48 -0
- data/README.md +243 -0
- data/bin/serif +81 -0
- data/lib/serif/admin_server.rb +186 -0
- data/lib/serif/config.rb +25 -0
- data/lib/serif/content_file.rb +154 -0
- data/lib/serif/draft.rb +58 -0
- data/lib/serif/markup_renderer.rb +24 -0
- data/lib/serif/post.rb +65 -0
- data/lib/serif/server.rb +32 -0
- data/lib/serif/site.rb +148 -0
- data/lib/serif.rb +19 -0
- data/serif.gemspec +28 -0
- data/statics/assets/js/jquery.autosize.js +175 -0
- data/statics/assets/js/mousetrap.min.js +28 -0
- data/statics/skeleton/_config.yml +14 -0
- data/statics/skeleton/_layouts/default.html +6 -0
- data/statics/skeleton/_templates/post.html +5 -0
- data/statics/skeleton/index.html +9 -0
- data/statics/templates/admin/edit_draft.liquid +54 -0
- data/statics/templates/admin/edit_post.liquid +36 -0
- data/statics/templates/admin/index.liquid +15 -0
- data/statics/templates/admin/layout.liquid +539 -0
- data/statics/templates/admin/new_draft.liquid +35 -0
- metadata +175 -6
- data/serif-stub.gemspec +0 -10
@@ -0,0 +1,154 @@
|
|
1
|
+
#
|
2
|
+
# ContentFile represents a file on the filesystem
|
3
|
+
# which contains the contents of a post, be it in
|
4
|
+
# draft form or otherwise.
|
5
|
+
#
|
6
|
+
# A ContentFile can determine its type based on
|
7
|
+
# the presence or absence of a "published"
|
8
|
+
# timestamp value.
|
9
|
+
#
|
10
|
+
|
11
|
+
module Serif
|
12
|
+
class ContentFile
|
13
|
+
attr_reader :path, :slug, :site
|
14
|
+
|
15
|
+
def initialize(site, path = nil)
|
16
|
+
@site = site
|
17
|
+
@path = path
|
18
|
+
|
19
|
+
if @path
|
20
|
+
# we have to parse out the source first so that we get necessary
|
21
|
+
# metadata like published vs. draft.
|
22
|
+
source = File.read(path).gsub(/\r?\n/, "\n")
|
23
|
+
source.force_encoding("UTF-8")
|
24
|
+
@source = Redhead::String[source]
|
25
|
+
|
26
|
+
dirname = File.basename(File.dirname(@path))
|
27
|
+
basename = File.basename(@path)
|
28
|
+
@slug = draft? ? basename : basename.split("-")[3..-1].join("-")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def slug=(str)
|
33
|
+
@slug = str
|
34
|
+
|
35
|
+
# if we're adding a slug and there's no path yet, then create the path.
|
36
|
+
# this will run for new drafts
|
37
|
+
|
38
|
+
if !@path
|
39
|
+
@path = File.expand_path("#{self.class.dirname}/#{@slug}")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def title
|
44
|
+
return nil if new?
|
45
|
+
headers[:title]
|
46
|
+
end
|
47
|
+
|
48
|
+
def title=(new_title)
|
49
|
+
if new?
|
50
|
+
@source = Redhead::String["title: #{new_title}\n\n"]
|
51
|
+
else
|
52
|
+
@source.headers[:title] = new_title
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def modified
|
57
|
+
File.mtime(@path)
|
58
|
+
end
|
59
|
+
|
60
|
+
def draft?
|
61
|
+
!published?
|
62
|
+
end
|
63
|
+
|
64
|
+
def published?
|
65
|
+
headers.key?(:created)
|
66
|
+
end
|
67
|
+
|
68
|
+
def content(include_headers = false)
|
69
|
+
include_headers ? "#{raw_headers}\n\n#{@source.to_s}" : @source.to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
def new?
|
73
|
+
!@source
|
74
|
+
end
|
75
|
+
|
76
|
+
def raw_headers
|
77
|
+
@source.headers.to_s
|
78
|
+
end
|
79
|
+
|
80
|
+
def created
|
81
|
+
return nil if new?
|
82
|
+
headers[:created].utc
|
83
|
+
end
|
84
|
+
|
85
|
+
def updated
|
86
|
+
return nil if new?
|
87
|
+
(headers[:updated] || created).utc
|
88
|
+
end
|
89
|
+
|
90
|
+
def headers
|
91
|
+
return {} unless @source
|
92
|
+
|
93
|
+
headers = @source.headers
|
94
|
+
converted_headers = {}
|
95
|
+
|
96
|
+
headers.each do |header|
|
97
|
+
key, value = header.key, header.value
|
98
|
+
|
99
|
+
if key == :created || key == :updated
|
100
|
+
value = Time.parse(value)
|
101
|
+
end
|
102
|
+
|
103
|
+
converted_headers[key] = value
|
104
|
+
end
|
105
|
+
|
106
|
+
converted_headers
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.rename(original_slug, new_slug)
|
110
|
+
raise if File.exist?("#{dirname}/#{new_slug}")
|
111
|
+
File.rename("#{dirname}/#{original_slug}", "#{dirname}/#{new_slug}")
|
112
|
+
end
|
113
|
+
|
114
|
+
def save(markdown = nil)
|
115
|
+
markdown ||= content if !new?
|
116
|
+
|
117
|
+
save_path = path || "#{self.class.dirname}/#{@slug}"
|
118
|
+
File.open(save_path, "w") do |f|
|
119
|
+
f.puts %Q{#{raw_headers}
|
120
|
+
|
121
|
+
#{markdown}}.strip
|
122
|
+
end
|
123
|
+
|
124
|
+
true # always return true for now
|
125
|
+
end
|
126
|
+
|
127
|
+
def [](header)
|
128
|
+
h = headers[header]
|
129
|
+
if h
|
130
|
+
h
|
131
|
+
else
|
132
|
+
raise "no such header #{header}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def inspect
|
137
|
+
%Q{<#{self.class} #{headers.inspect}>}
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.all
|
141
|
+
Post.all + Draft.all
|
142
|
+
end
|
143
|
+
|
144
|
+
protected
|
145
|
+
|
146
|
+
def set_publish_time(time)
|
147
|
+
@source.headers[:created] = time.xmlschema
|
148
|
+
end
|
149
|
+
|
150
|
+
def set_updated_time(time)
|
151
|
+
@source.headers[:updated] = time.xmlschema
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
data/lib/serif/draft.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module Serif
|
2
|
+
class Draft < ContentFile
|
3
|
+
def self.dirname
|
4
|
+
"_drafts"
|
5
|
+
end
|
6
|
+
|
7
|
+
def delete!
|
8
|
+
FileUtils.mkdir_p("_trash")
|
9
|
+
File.rename(@path, File.expand_path("_trash/#{Time.now.to_i}-#{slug}"))
|
10
|
+
end
|
11
|
+
|
12
|
+
def publish!
|
13
|
+
publish_time = Time.now
|
14
|
+
date = Time.now.strftime("%Y-%m-%d")
|
15
|
+
filename = "#{date}-#{slug}"
|
16
|
+
full_published_path = File.expand_path("#{Post.dirname}/#{filename}")
|
17
|
+
|
18
|
+
raise "conflict, post exists already" if File.exist?(full_published_path)
|
19
|
+
|
20
|
+
set_publish_time(publish_time)
|
21
|
+
save
|
22
|
+
|
23
|
+
File.rename(path, full_published_path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_liquid
|
27
|
+
h = {
|
28
|
+
"title" => title,
|
29
|
+
"content" => content,
|
30
|
+
"slug" => slug,
|
31
|
+
"type" => "draft",
|
32
|
+
"draft" => draft?,
|
33
|
+
"published" => published?
|
34
|
+
}
|
35
|
+
|
36
|
+
headers.each do |key, value|
|
37
|
+
h[key] = value
|
38
|
+
end
|
39
|
+
|
40
|
+
h
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.exist?(site, slug)
|
44
|
+
all(site).any? { |d| d.slug == slug }
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.all(site)
|
48
|
+
files = Dir[File.join(site.directory, dirname, "*")].select { |f| File.file?(f) }.map { |f| File.expand_path(f) }
|
49
|
+
files.map { |f| new(site, f) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.from_slug(site, slug)
|
53
|
+
path = File.expand_path(File.join(site.directory, dirname, slug))
|
54
|
+
p path
|
55
|
+
new(site, path)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Serif
|
2
|
+
class MarkupRenderer < Redcarpet::Render::SmartyHTML
|
3
|
+
def block_code(code, language)
|
4
|
+
# bypass it all to avoid sticking higlihting markup on stuff with no language
|
5
|
+
return Redcarpet::Markdown.new(Redcarpet::Render::SmartyHTML, fenced_code_blocks: true).render(%Q{```
|
6
|
+
#{code}
|
7
|
+
```}).strip unless language
|
8
|
+
|
9
|
+
out = Pygments.highlight(code, lexer: language)
|
10
|
+
|
11
|
+
# first, get rid of the div, since we want
|
12
|
+
# to stick the class onto the <pre>, to stay
|
13
|
+
# clean markup-wise.
|
14
|
+
out.sub!(/^<div[^>]*>/, "")
|
15
|
+
out.strip!
|
16
|
+
out.sub!(/<\/div>\z/, "")
|
17
|
+
|
18
|
+
out.sub!(/^<pre>/, "<pre#{" class=\"highlight\""}><code>")
|
19
|
+
out.sub!(/<\/pre>\z/, "</code></pre>\n")
|
20
|
+
|
21
|
+
out
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/serif/post.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
module Serif
|
4
|
+
class Post < ContentFile
|
5
|
+
def self.dirname
|
6
|
+
"_posts"
|
7
|
+
end
|
8
|
+
|
9
|
+
def url
|
10
|
+
permalink_style = headers[:permalink] || site.config.permalink
|
11
|
+
|
12
|
+
filename_parts = File.basename(path).split("-")
|
13
|
+
|
14
|
+
parts = {
|
15
|
+
"title" => slug,
|
16
|
+
"year" => filename_parts[0],
|
17
|
+
"month" => filename_parts[1],
|
18
|
+
"day" => filename_parts[2]
|
19
|
+
}
|
20
|
+
|
21
|
+
output = permalink_style
|
22
|
+
|
23
|
+
parts.each do |placeholder, value|
|
24
|
+
output = output.gsub(Regexp.quote(":" + placeholder), value)
|
25
|
+
end
|
26
|
+
|
27
|
+
output
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.all(site)
|
31
|
+
files = Dir[File.join(site.directory, dirname, "*")].select { |f| File.file?(f) }.map { |f| File.expand_path(f) }
|
32
|
+
files.map { |f| new(site, f) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.from_slug(site, slug)
|
36
|
+
all(site).find { |p| p.slug == slug }
|
37
|
+
end
|
38
|
+
|
39
|
+
def save(markdown)
|
40
|
+
# update the timestamp if the content has actually changed
|
41
|
+
set_updated_time(Time.now) unless markdown.strip == content.strip
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_liquid
|
46
|
+
h = {
|
47
|
+
"title" => title,
|
48
|
+
"created" => created,
|
49
|
+
"updated" => updated,
|
50
|
+
"content" => content,
|
51
|
+
"slug" => slug,
|
52
|
+
"url" => url,
|
53
|
+
"type" => "post",
|
54
|
+
"draft" => draft?,
|
55
|
+
"published" => published?
|
56
|
+
}
|
57
|
+
|
58
|
+
headers.each do |key, value|
|
59
|
+
h[key.to_s] = value
|
60
|
+
end
|
61
|
+
|
62
|
+
h
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/serif/server.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require "sinatra/base"
|
2
|
+
require "fileutils"
|
3
|
+
require "rack/rewrite"
|
4
|
+
|
5
|
+
module Serif
|
6
|
+
class DevelopmentServer
|
7
|
+
class DevApp < Sinatra::Base
|
8
|
+
set :public_folder, Dir.pwd
|
9
|
+
|
10
|
+
get "/" do
|
11
|
+
File.read(File.expand_path("_site/index.html"))
|
12
|
+
end
|
13
|
+
|
14
|
+
# it seems Rack::Rewrite doesn't like public_folder files, so here we are
|
15
|
+
get "*" do
|
16
|
+
File.read(Dir[File.expand_path("_site#{params[:splat].join("/")}.*")].first)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :source_directory
|
21
|
+
|
22
|
+
def initialize(source_directory)
|
23
|
+
@source_directory = source_directory
|
24
|
+
end
|
25
|
+
|
26
|
+
def start
|
27
|
+
FileUtils.cd @source_directory
|
28
|
+
app = Sinatra.new(DevApp)
|
29
|
+
app.run!(:port => 8000)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/serif/site.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
class StandardFilterCheck
|
2
|
+
include Liquid::StandardFilters
|
3
|
+
|
4
|
+
def date_supports_now?
|
5
|
+
date("now", "%Y") == Time.now.year
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
if StandardFilterCheck.new.date_supports_now?
|
10
|
+
puts "NOTICE! 'now' is supported by 'date' filter. Remove the patch"
|
11
|
+
sleep 5 # incur a penalty
|
12
|
+
else
|
13
|
+
module Liquid::StandardFilters
|
14
|
+
alias_method :date_orig, :date
|
15
|
+
|
16
|
+
def date(input, format)
|
17
|
+
input == "now" ? date_orig(Time.now, format) : date_orig(input, format)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Filters
|
23
|
+
def strip(input)
|
24
|
+
input.strip
|
25
|
+
end
|
26
|
+
|
27
|
+
def encode_uri_component(string)
|
28
|
+
CGI.escape(string)
|
29
|
+
end
|
30
|
+
|
31
|
+
def markdown(body)
|
32
|
+
Redcarpet::Markdown.new(Serif::MarkupRenderer, fenced_code_blocks: true).render(body).strip
|
33
|
+
end
|
34
|
+
|
35
|
+
def xmlschema(input)
|
36
|
+
input.xmlschema
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Liquid::Template.register_filter(Filters)
|
41
|
+
|
42
|
+
module Serif
|
43
|
+
class Site
|
44
|
+
def initialize(source_directory)
|
45
|
+
@source_directory = source_directory
|
46
|
+
end
|
47
|
+
|
48
|
+
def directory
|
49
|
+
@source_directory
|
50
|
+
end
|
51
|
+
|
52
|
+
def posts
|
53
|
+
Post.all(self)
|
54
|
+
end
|
55
|
+
|
56
|
+
def drafts
|
57
|
+
Draft.all(self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def config
|
61
|
+
Serif::Config.new(File.join(@source_directory, "_config.yml"))
|
62
|
+
end
|
63
|
+
|
64
|
+
def site_path(path)
|
65
|
+
File.join("_site", path)
|
66
|
+
end
|
67
|
+
|
68
|
+
def tmp_path(path)
|
69
|
+
File.join("tmp", site_path(path))
|
70
|
+
end
|
71
|
+
|
72
|
+
def latest_update_time
|
73
|
+
most_recent = posts.max_by { |p| p.updated }
|
74
|
+
most_recent ? most_recent.updated : Time.now
|
75
|
+
end
|
76
|
+
|
77
|
+
def bypass?(filename)
|
78
|
+
!%w[.html .xml].include?(File.extname(filename))
|
79
|
+
end
|
80
|
+
|
81
|
+
# TODO: fix all these File.join calls
|
82
|
+
def generate
|
83
|
+
FileUtils.cd(@source_directory)
|
84
|
+
|
85
|
+
FileUtils.rm_rf("tmp/_site")
|
86
|
+
FileUtils.mkdir_p("tmp/_site")
|
87
|
+
|
88
|
+
files = Dir["**/*"].select { |f| f !~ /\A_/ && File.file?(f) }
|
89
|
+
|
90
|
+
layout = Liquid::Template.parse(File.read("_layouts/default.html"))
|
91
|
+
posts = self.posts.sort_by { |entry| entry.created }.reverse
|
92
|
+
|
93
|
+
files.each do |path|
|
94
|
+
puts "Processing file: #{path}"
|
95
|
+
|
96
|
+
dirname = File.dirname(path)
|
97
|
+
filename = File.basename(path)
|
98
|
+
|
99
|
+
FileUtils.mkdir_p(tmp_path(dirname))
|
100
|
+
if bypass?(filename)
|
101
|
+
FileUtils.cp(path, tmp_path(path))
|
102
|
+
else
|
103
|
+
File.open(tmp_path(path), "w") do |f|
|
104
|
+
file = File.read(path)
|
105
|
+
title = nil
|
106
|
+
layout_option = :default
|
107
|
+
|
108
|
+
begin
|
109
|
+
file_with_headers = Redhead::String[File.read(path)]
|
110
|
+
title = file_with_headers.headers[:title] && file_with_headers.headers[:title].value
|
111
|
+
layout_option = file_with_headers.headers[:layout] && file_with_headers.headers[:layout].value
|
112
|
+
|
113
|
+
# all good? use the headered string
|
114
|
+
file = file_with_headers
|
115
|
+
rescue => e
|
116
|
+
puts "Warning! Problem trying to get headers out of #{path}"
|
117
|
+
puts e
|
118
|
+
# stick with what we've got!
|
119
|
+
end
|
120
|
+
|
121
|
+
if layout_option == "none"
|
122
|
+
f.puts Liquid::Template.parse(file.to_s).render!("site" => { "posts" => posts, "latest_update_time" => latest_update_time })
|
123
|
+
else
|
124
|
+
f.puts layout.render!("page" => { "title" => [title].compact }, "content" => Liquid::Template.parse(file.to_s).render!("site" => { "posts" => posts, "latest_update_time" => latest_update_time }))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
posts.each do |post|
|
131
|
+
puts "Processing post: #{post.path}"
|
132
|
+
|
133
|
+
FileUtils.mkdir_p(tmp_path(File.dirname(post.url)))
|
134
|
+
|
135
|
+
File.open(tmp_path(post.url + ".html"), "w") do |f|
|
136
|
+
f.puts layout.render!("page" => { "title" => ["Posts", "#{post.title}"] }, "content" => Liquid::Template.parse(File.read("_templates/post.html")).render!("post" => post))
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
if Dir.exist?("_site")
|
141
|
+
FileUtils.mv("_site", "/tmp/_site.#{Time.now.strftime("%Y-%m-%d-%H-%M-%S")}")
|
142
|
+
end
|
143
|
+
|
144
|
+
FileUtils.mv("tmp/_site", ".") && FileUtils.rm_rf("tmp/_site")
|
145
|
+
FileUtils.rmdir("tmp")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/lib/serif.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "time"
|
2
|
+
|
3
|
+
require "liquid"
|
4
|
+
require "redcarpet"
|
5
|
+
require "pygments.rb"
|
6
|
+
require "redhead"
|
7
|
+
|
8
|
+
require "cgi"
|
9
|
+
require "digest"
|
10
|
+
|
11
|
+
require "serif/content_file"
|
12
|
+
require "serif/post"
|
13
|
+
require "serif/draft"
|
14
|
+
require "serif/markup_renderer"
|
15
|
+
require "serif/site"
|
16
|
+
require "serif/config"
|
17
|
+
|
18
|
+
module Serif
|
19
|
+
end
|
data/serif.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "serif"
|
3
|
+
s.version = "0.1.2"
|
4
|
+
s.authors = ["Adam Prescott"]
|
5
|
+
s.email = ["adam@aprescott.com"]
|
6
|
+
s.homepage = "https://github.com/aprescott/serif"
|
7
|
+
s.summary = "Simple file-based blogging system."
|
8
|
+
s.description = "Serif is a simple file-based blogging system which generates static content and allows dynamic editing through an interface."
|
9
|
+
s.files = Dir["{lib/**/*,statics/**/*,bin/*}"] + %w[serif.gemspec LICENSE Gemfile Gemfile.lock README.md]
|
10
|
+
s.require_path = "lib"
|
11
|
+
s.bindir = "bin"
|
12
|
+
s.executables = "serif"
|
13
|
+
#s.test_files = Dir["test/*"]
|
14
|
+
|
15
|
+
[
|
16
|
+
"rake", "~> 0.9",
|
17
|
+
"rack", "~> 1.0",
|
18
|
+
"yui-compressor", ">= 0",
|
19
|
+
"redcarpet", "~> 2.2",
|
20
|
+
"pygments.rb", "~> 0.3",
|
21
|
+
"sinatra", "~> 1.3",
|
22
|
+
"redhead", "~> 0.0.6",
|
23
|
+
"liquid", "~> 2.4",
|
24
|
+
"slop", "~> 3.3"
|
25
|
+
].each_slice(2) do |name, version|
|
26
|
+
s.add_runtime_dependency(name, version)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
// Autosize 1.13 - jQuery plugin for textareas
|
2
|
+
// (c) 2012 Jack Moore - jacklmoore.com
|
3
|
+
// license: www.opensource.org/licenses/mit-license.php
|
4
|
+
|
5
|
+
(function ($) {
|
6
|
+
var
|
7
|
+
hidden = 'hidden',
|
8
|
+
borderBox = 'border-box',
|
9
|
+
lineHeight = 'lineHeight',
|
10
|
+
copy = '<textarea tabindex="-1" style="position:absolute; top:-9999px; left:-9999px; right:auto; bottom:auto; -moz-box-sizing:content-box; -webkit-box-sizing:content-box; box-sizing:content-box; word-wrap:break-word; height:0 !important; min-height:0 !important; overflow:hidden;"/>',
|
11
|
+
// line-height is omitted because IE7/IE8 doesn't return the correct value.
|
12
|
+
copyStyle = [
|
13
|
+
'fontFamily',
|
14
|
+
'fontSize',
|
15
|
+
'fontWeight',
|
16
|
+
'fontStyle',
|
17
|
+
'letterSpacing',
|
18
|
+
'textTransform',
|
19
|
+
'wordSpacing',
|
20
|
+
'textIndent'
|
21
|
+
],
|
22
|
+
oninput = 'oninput',
|
23
|
+
onpropertychange = 'onpropertychange',
|
24
|
+
test = $(copy)[0];
|
25
|
+
|
26
|
+
// For testing support in old FireFox
|
27
|
+
test.setAttribute(oninput, "return");
|
28
|
+
|
29
|
+
if ($.isFunction(test[oninput]) || onpropertychange in test) {
|
30
|
+
|
31
|
+
// test that line-height can be accurately copied to avoid
|
32
|
+
// incorrect value reporting in old IE and old Opera
|
33
|
+
$(test).css(lineHeight, '99px');
|
34
|
+
if ($(test).css(lineHeight) === '99px') {
|
35
|
+
copyStyle.push(lineHeight);
|
36
|
+
}
|
37
|
+
|
38
|
+
$.fn.autosize = function (options) {
|
39
|
+
options = options || {};
|
40
|
+
|
41
|
+
return this.each(function () {
|
42
|
+
var
|
43
|
+
ta = this,
|
44
|
+
$ta = $(ta),
|
45
|
+
mirror,
|
46
|
+
minHeight = $ta.height(),
|
47
|
+
maxHeight = parseInt($ta.css('maxHeight'), 10),
|
48
|
+
active,
|
49
|
+
i = copyStyle.length,
|
50
|
+
resize,
|
51
|
+
boxOffset = 0,
|
52
|
+
value = ta.value,
|
53
|
+
callback = $.isFunction(options.callback);
|
54
|
+
|
55
|
+
if ($ta.css('box-sizing') === borderBox || $ta.css('-moz-box-sizing') === borderBox || $ta.css('-webkit-box-sizing') === borderBox){
|
56
|
+
boxOffset = $ta.outerHeight() - $ta.height();
|
57
|
+
}
|
58
|
+
|
59
|
+
if ($ta.data('mirror') || $ta.data('ismirror')) {
|
60
|
+
// if autosize has already been applied, exit.
|
61
|
+
// if autosize is being applied to a mirror element, exit.
|
62
|
+
return;
|
63
|
+
} else {
|
64
|
+
mirror = $(copy).data('ismirror', true).addClass(options.className || 'autosizejs')[0];
|
65
|
+
|
66
|
+
resize = $ta.css('resize') === 'none' ? 'none' : 'horizontal';
|
67
|
+
|
68
|
+
$ta.data('mirror', $(mirror)).css({
|
69
|
+
overflow: hidden,
|
70
|
+
overflowY: hidden,
|
71
|
+
wordWrap: 'break-word',
|
72
|
+
resize: resize
|
73
|
+
});
|
74
|
+
}
|
75
|
+
|
76
|
+
// Opera returns '-1px' when max-height is set to 'none'.
|
77
|
+
maxHeight = maxHeight && maxHeight > 0 ? maxHeight : 9e4;
|
78
|
+
|
79
|
+
// Using mainly bare JS in this function because it is going
|
80
|
+
// to fire very often while typing, and needs to very efficient.
|
81
|
+
function adjust() {
|
82
|
+
var height, overflow, original;
|
83
|
+
|
84
|
+
// the active flag keeps IE from tripping all over itself. Otherwise
|
85
|
+
// actions in the adjust function will cause IE to call adjust again.
|
86
|
+
if (!active) {
|
87
|
+
active = true;
|
88
|
+
mirror.value = ta.value;
|
89
|
+
mirror.style.overflowY = ta.style.overflowY;
|
90
|
+
original = parseInt(ta.style.height,10);
|
91
|
+
|
92
|
+
// Update the width in case the original textarea width has changed
|
93
|
+
mirror.style.width = $ta.css('width');
|
94
|
+
|
95
|
+
// Needed for IE to reliably return the correct scrollHeight
|
96
|
+
mirror.scrollTop = 0;
|
97
|
+
|
98
|
+
// Set a very high value for scrollTop to be sure the
|
99
|
+
// mirror is scrolled all the way to the bottom.
|
100
|
+
mirror.scrollTop = 9e4;
|
101
|
+
|
102
|
+
height = mirror.scrollTop;
|
103
|
+
overflow = hidden;
|
104
|
+
if (height > maxHeight) {
|
105
|
+
height = maxHeight;
|
106
|
+
overflow = 'scroll';
|
107
|
+
} else if (height < minHeight) {
|
108
|
+
height = minHeight;
|
109
|
+
}
|
110
|
+
height += boxOffset;
|
111
|
+
ta.style.overflowY = overflow;
|
112
|
+
|
113
|
+
if (original !== height) {
|
114
|
+
ta.style.height = height + 'px';
|
115
|
+
if (callback) {
|
116
|
+
options.callback.call(ta);
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
// This small timeout gives IE a chance to draw it's scrollbar
|
121
|
+
// before adjust can be run again (prevents an infinite loop).
|
122
|
+
setTimeout(function () {
|
123
|
+
active = false;
|
124
|
+
}, 1);
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
// mirror is a duplicate textarea located off-screen that
|
129
|
+
// is automatically updated to contain the same text as the
|
130
|
+
// original textarea. mirror always has a height of 0.
|
131
|
+
// This gives a cross-browser supported way getting the actual
|
132
|
+
// height of the text, through the scrollTop property.
|
133
|
+
while (i--) {
|
134
|
+
mirror.style[copyStyle[i]] = $ta.css(copyStyle[i]);
|
135
|
+
}
|
136
|
+
|
137
|
+
$('body').append(mirror);
|
138
|
+
|
139
|
+
if (onpropertychange in ta) {
|
140
|
+
if (oninput in ta) {
|
141
|
+
// Detects IE9. IE9 does not fire onpropertychange or oninput for deletions,
|
142
|
+
// so binding to onkeyup to catch most of those occassions. There is no way that I
|
143
|
+
// know of to detect something like 'cut' in IE9.
|
144
|
+
ta[oninput] = ta.onkeyup = adjust;
|
145
|
+
} else {
|
146
|
+
// IE7 / IE8
|
147
|
+
ta[onpropertychange] = adjust;
|
148
|
+
}
|
149
|
+
} else {
|
150
|
+
// Modern Browsers
|
151
|
+
ta[oninput] = adjust;
|
152
|
+
|
153
|
+
// The textarea overflow is now hidden. But Chrome doesn't reflow the text after the scrollbars are removed.
|
154
|
+
// This is a hack to get Chrome to reflow it's text.
|
155
|
+
ta.value = '';
|
156
|
+
ta.value = value;
|
157
|
+
}
|
158
|
+
|
159
|
+
$(window).resize(adjust);
|
160
|
+
|
161
|
+
// Allow for manual triggering if needed.
|
162
|
+
$ta.bind('autosize', adjust);
|
163
|
+
|
164
|
+
// Call adjust in case the textarea already contains text.
|
165
|
+
adjust();
|
166
|
+
});
|
167
|
+
};
|
168
|
+
} else {
|
169
|
+
// Makes no changes for older browsers (FireFox3- and Safari4-)
|
170
|
+
$.fn.autosize = function (callback) {
|
171
|
+
return this;
|
172
|
+
};
|
173
|
+
}
|
174
|
+
|
175
|
+
}(jQuery));
|