amber 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,66 @@
1
+ #
2
+ # A Layout is similar to a layout in Rails (a template that decorates the pages)
3
+ #
4
+
5
+ gem 'tilt', '>= 2.0.0'
6
+ require 'tilt'
7
+ require 'haml'
8
+
9
+ #Haml::Options.defaults[:format] = :html5
10
+
11
+ module Amber
12
+ module Render
13
+
14
+ class Layout
15
+ def self.load(layout_dir=nil)
16
+ @layout_dirs ||= []
17
+ @layout_dirs << layout_dir if layout_dir
18
+ reload
19
+ end
20
+
21
+ def self.reload
22
+ @layouts ||= {}
23
+ @layouts['default'] = DefaultLayout.new
24
+ @layout_dirs.each do |dir|
25
+ Dir.glob("#{dir}/*").each do |layout_file|
26
+ name = File.basename(layout_file).sub(/^([^\.]*).*$/, "\\1")
27
+ @layouts[name] = Layout.new(layout_file)
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.[](layout)
33
+ @layouts[layout]
34
+ end
35
+
36
+ def initialize(file_path, &block)
37
+ if file_path =~ /\.haml$/
38
+ @template = Tilt::HamlTemplate.new(file_path, {:format => :html5})
39
+ else
40
+ @template = Tilt.new(file_path, &block)
41
+ end
42
+ end
43
+
44
+ def render(view, &block)
45
+ @template.render(view, &block)
46
+ end
47
+ end
48
+
49
+ class DefaultLayout < Layout
50
+ def initialize
51
+ @template = Tilt::StringTemplate.new {DEFAULT}
52
+ end
53
+ DEFAULT = '<!DOCTYPE html>
54
+ <html>
55
+ <head>
56
+ <title>#{ @page.nav_title } - #{@site.title}</title>
57
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
58
+ </head>
59
+ <body>
60
+ #{ yield }
61
+ </body>
62
+ </html>'
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,383 @@
1
+ # encoding: utf-8
2
+
3
+ require 'nokogiri'
4
+ require 'cgi'
5
+
6
+ #
7
+ # Generates a table of contents for any HTML markup, and adds anchors to headings.
8
+ #
9
+
10
+ module Amber::Render
11
+
12
+ ##
13
+ ## TABLE OF CONTENTS
14
+ ##
15
+
16
+ class TableOfContents
17
+ #
18
+ # options:
19
+ # :content_selector (css selector for headings, nokogiri backend only)
20
+ # :href_base -- use this href for the toc links
21
+ # :numeric_prefix -- prefix toc entries and headings with numeric counter (e.g. 1.1.0, 1.2.0, ...)
22
+ #
23
+ def initialize(html, options = {})
24
+ @html = html
25
+ @toc = TocItem.new
26
+ @levels = {"h1" => 0, "h2" => 0, "h3" => 0, "h4" => 0}
27
+ @heading_anchors = {}
28
+ @options = options
29
+ @options[:tag] ||= 'ol'
30
+ end
31
+
32
+ def to_html
33
+ parse_doc unless @parsed
34
+ # override this!
35
+ end
36
+
37
+ def to_toc
38
+ parse_doc unless @parsed
39
+ # override this!
40
+ end
41
+
42
+ private
43
+
44
+ def parse_doc
45
+ each_heading(@html) do |heading, heading_text|
46
+ heading_anchor = anchor_text(heading_text)
47
+ heading_text = strip_anchors(heading_text)
48
+ if @options[:numeric_prefix]
49
+ increment_level(heading)
50
+ heading_text = level_text + " " + heading_text
51
+ end
52
+ @toc.add_heading(heading, heading_text, heading_anchor)
53
+ '<a name="%s"></a>%s' % [heading_anchor, heading_text]
54
+ end
55
+ @parsed = true
56
+ end
57
+
58
+ #
59
+ # returns anchor text from heading text.
60
+ # e.g. First Heading! => first-heading
61
+ #
62
+ # if there are duplicates, they get numbered:
63
+ # heading => heading
64
+ # heading => heading-2
65
+ # heading => heading-3
66
+ #
67
+ def anchor_text(heading_text)
68
+ text = nameize(strip_html_tags(heading_text))
69
+ text_with_suffix = text
70
+ i = 2
71
+ while @heading_anchors[text_with_suffix]
72
+ text_with_suffix = "#{text}-#{i}"
73
+ i+=1
74
+ end
75
+ @heading_anchors[text_with_suffix] = true
76
+ text_with_suffix
77
+ end
78
+
79
+ #
80
+ # convert any string to one suitable for a url.
81
+ # resist the urge to translit non-ascii slugs to ascii.
82
+ # it is always much better to keep strings as utf8.
83
+ #
84
+ def nameize(str)
85
+ str = str.dup
86
+ str.gsub!(/&(\w{2,6}?|#[0-9A-Fa-f]{2,6});/,'') # remove html entitities
87
+ str.gsub!(/[^- [[:word:]]]/u, '') # remove non-word characters (using unicode definition of a word char)
88
+ str.strip!
89
+ str.downcase! # upper case characters in urls are confusing
90
+ str.gsub!(/\ +/u, '-') # spaces to dashes, preferred separator char everywhere
91
+ CGI.escape(str)
92
+ end
93
+
94
+ # removes all html markup
95
+ def strip_html_tags(html)
96
+ Nokogiri::HTML::DocumentFragment.parse(html, 'UTF-8').children.collect{|child| child.inner_text}.join
97
+ end
98
+
99
+ # remove <a name='x'></a> from html, but leaves all other tags in place.
100
+ def strip_anchors(html)
101
+ Nokogiri::HTML::DocumentFragment.parse(html, 'UTF-8').children.collect{|child|
102
+ if child.name == "text"
103
+ child.inner_text
104
+ elsif child.name != 'a' || !child.attributes.detect{|atr| atr[0] == 'name'}
105
+ child.to_s
106
+ end
107
+ }.join
108
+ end
109
+
110
+ #
111
+ # prefix headings with text like 1.2.1, if :numeric_prefix => true
112
+ #
113
+ def level_text
114
+ [@levels["h1"], @levels["h2"], @levels["h3"], @levels["h4"]].join(".").gsub(/\.0/, "")
115
+ end
116
+
117
+ #
118
+ # keeps a counter of the latest heading at each level
119
+ #
120
+ def increment_level(heading)
121
+ @levels[heading] += 1
122
+ @levels["h2"] = 0 if heading == "h1"
123
+ @levels["h3"] = 0 if heading == "h1" || heading == "h2"
124
+ @levels["h4"] = 0 if heading == "h1" || heading == "h2" || heading == "h3"
125
+ end
126
+
127
+ def each_heading(html, &block)
128
+ raise 'override me'
129
+ end
130
+ end
131
+
132
+ ##
133
+ ## NOKOGIRI TOC
134
+ ##
135
+
136
+ class NokogiriTableOfContents < TableOfContents
137
+ def to_html
138
+ super
139
+ @nokogiri_doc.to_html.gsub(/(<h\d.*?>)\n/, '\1').gsub(/\n(<\/h\d.*?>)/, '\1')
140
+ end
141
+
142
+ def to_toc
143
+ super
144
+ ul = Nokogiri::XML::Node.new(@options[:tag], Nokogiri::HTML.fragment(""))
145
+ @toc.populate_node(ul, @options)
146
+ ul.to_pretty_html
147
+ end
148
+
149
+ private
150
+
151
+ def each_heading(html, &block)
152
+ @nokogiri_doc = Nokogiri::HTML.fragment(html, "UTF-8")
153
+ if @options[:content_selector]
154
+ selector = @levels.keys.map {|h| "#{@options[:content_selector]} #{h}" }.join(",")
155
+ else
156
+ selector = @levels.keys.join(",")
157
+ end
158
+ @nokogiri_doc.css(selector).each do |node|
159
+ node.inner_html = yield(node.name, node.inner_html)
160
+ end
161
+ end
162
+ end
163
+
164
+ ##
165
+ ## REGEX TOC
166
+ ##
167
+
168
+ class RegexTableOfContents < TableOfContents
169
+ def to_html
170
+ super
171
+ @new_html
172
+ end
173
+
174
+ def to_toc
175
+ super
176
+ @toc.to_html(@options)
177
+ end
178
+
179
+ private
180
+
181
+ HEADING_EX = %r{
182
+ <\s*((h\d).*?)\s*> # match starting <h1>
183
+ (.+)? # match innner text
184
+ <\s*\/\2\s*> # match closing </h1>
185
+ }x
186
+
187
+ def each_heading(html, &block)
188
+ @new_html = html.gsub(HEADING_EX) do |match|
189
+ "<%s>%s</%s>" % [$1, yield($2, $3), $2]
190
+ end
191
+ end
192
+ end
193
+
194
+ ##
195
+ ## TOC ITEM
196
+ ##
197
+ ## A tree of TocItems composes the table of contents outline.
198
+ ##
199
+
200
+ class TocItem
201
+ attr_reader :children, :level, :text, :anchor
202
+
203
+ def initialize(heading='h0', text=nil, anchor=nil)
204
+ @level = heading[1].to_i if heading.is_a?(String)
205
+ @text = text
206
+ @anchor = anchor
207
+ @children = []
208
+ end
209
+
210
+ def add_heading(heading, heading_text, heading_anchor)
211
+ self.parent_for(heading).children << TocItem.new(heading, heading_text, heading_anchor)
212
+ end
213
+
214
+ #
215
+ # generates nokogiri html node tree from this toc
216
+ #
217
+ def populate_node(node, options)
218
+ @children.each do |item|
219
+ li = node.document.create_element("li")
220
+ li.add_child(li.document.create_element("a", item.text, :href => "#{options[:href_base]}##{item.anchor}"))
221
+ if item.children.any?
222
+ ul = li.document.create_element(options[:tag])
223
+ item.populate_node(ul, options)
224
+ li.add_child(ul)
225
+ end
226
+ node.add_child(li)
227
+ end
228
+ end
229
+
230
+ #
231
+ # generates html string from this toc
232
+ #
233
+ def to_html(options={})
234
+ html = []
235
+ tag = options[:tag]
236
+ indent = options[:indent] || 0
237
+ str = options[:indent_str] || " "
238
+ html << '%s<%s>' % [(str*indent), tag]
239
+ @children.each do |item|
240
+ html << '%s<li>' % (str*(indent+1))
241
+ html << '%s<a href="%s#%s">%s</a>' % [str*(indent+2), options[:href_base], item.anchor, item.text]
242
+ if item.children.any?
243
+ html << item.to_html({
244
+ :indent => indent+2,
245
+ :indent_str => str,
246
+ :tag => tag,
247
+ :href_base => options[:href_base]
248
+ })
249
+ end
250
+ html << '%s</li>' % (str*(indent+1))
251
+ end
252
+ html << '%s</%s>' % [(str*indent), tag]
253
+ html.join("\n")
254
+ end
255
+
256
+ #
257
+ # Returns the appropriate TocItem for appending a new item
258
+ # at a particular heading level.
259
+ #
260
+ def parent_for(heading)
261
+ heading = heading[1].to_i if heading.is_a?(String)
262
+ if children.any? && children.last.level < heading
263
+ children.last.parent_for(heading)
264
+ else
265
+ self
266
+ end
267
+ end
268
+ end
269
+
270
+ end
271
+
272
+ class Nokogiri::XML::Node
273
+ def to_pretty_html(indent=0)
274
+ indent_str = " " * indent
275
+ children_html = []
276
+ text_html = nil
277
+ if children.size == 1 && children.first.name == "text"
278
+ text_html = children.first.content
279
+ else
280
+ children.each do |child|
281
+ if child.name == "text"
282
+ children_html << "#{" " * (indent+1)}#{child.content}" if !child.content.empty?
283
+ else
284
+ children_html << child.to_pretty_html(indent+1)
285
+ end
286
+ end
287
+ end
288
+ attrs = []
289
+ attributes.each do |attribute|
290
+ attrs << %(#{attribute[0]}="#{attribute[1]}")
291
+ end
292
+ if attrs.any?
293
+ attr_html = " " + attrs.join(' ')
294
+ else
295
+ attr_html = ""
296
+ end
297
+ html = []
298
+ if text_html
299
+ html << "#{indent_str}<#{name}#{attr_html}>#{text_html}</#{name}>"
300
+ elsif children_html.any?
301
+ html << "#{indent_str}<#{name}#{attr_html}>"
302
+ html += children_html
303
+ html << "#{indent_str}</#{name}>"
304
+ else
305
+ html << "#{indent_str}<#{name}#{attr_html}></#{name}>"
306
+ end
307
+ html.join("\n")
308
+ end
309
+ end
310
+
311
+
312
+ =begin
313
+
314
+ AN ATTEMPT TO GET NOKOGIRI TO OUTPUT REASONABLE HTML5. NO LUCK.
315
+
316
+ #
317
+ # convert a Nokogiri::HTML::Document into well formatted html.
318
+ # unfortunately, Nokogiri formatting only works on complete documents, so we strip away the <html> tags. :(
319
+ #
320
+ def format_doc(doc)
321
+ INDENT_XSLT.apply_to(doc).to_s.sub("<!DOCTYPE html>\n<html><body>", '').sub('</body></html>', '')
322
+ end
323
+
324
+ # from https://github.com/jarijokinen/html5-beautifier/blob/master/lib/html5-beautifier/xslt/html5-beautifier.xslt
325
+ # MIT License
326
+ INDENT_XSLT_STRING = <<EOF
327
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
328
+ <xsl:output method="html" omit-xml-declaration="yes" encoding="utf-8" />
329
+ <xsl:param name="indent-increment" select="'__INDENT_STRING__'" />
330
+
331
+ <xsl:template name="newline">
332
+ <xsl:text disable-output-escaping="yes">
333
+ </xsl:text>
334
+ </xsl:template>
335
+
336
+ <xsl:template match="/">
337
+ <xsl:text disable-output-escaping="yes">&lt;!DOCTYPE html></xsl:text>
338
+ <xsl:apply-templates />
339
+ </xsl:template>
340
+
341
+ <xsl:template match="comment() | processing-instruction()">
342
+ <xsl:param name="indent" select="''" />
343
+ <xsl:call-template name="newline" />
344
+ <xsl:value-of select="$indent" />
345
+ <xsl:copy />
346
+ </xsl:template>
347
+
348
+ <xsl:template match="text()">
349
+ <xsl:param name="indent" select="''" />
350
+ <xsl:call-template name="newline" />
351
+ <xsl:value-of select="$indent" />
352
+ <xsl:value-of select="normalize-space(.)" />
353
+ </xsl:template>
354
+
355
+ <xsl:template match="text()[normalize-space(.)='']" />
356
+
357
+ <xsl:template match="*">
358
+ <xsl:param name="indent" select="''" />
359
+ <xsl:call-template name="newline" />
360
+ <xsl:value-of select="$indent" />
361
+ <xsl:choose>
362
+ <xsl:when test="count(child::*) > 0 and __EXCLUDE_ELEMENTS__">
363
+ <xsl:copy>
364
+ <xsl:copy-of select="@*" />
365
+ <xsl:apply-templates select="*|text()">
366
+ <xsl:with-param name="indent" select="concat($indent, $indent-increment)" />
367
+ </xsl:apply-templates>
368
+ <xsl:call-template name="newline" />
369
+ <xsl:value-of select="$indent" />
370
+ </xsl:copy>
371
+ </xsl:when>
372
+ <xsl:otherwise>
373
+ <xsl:copy-of select="." />
374
+ </xsl:otherwise>
375
+ </xsl:choose>
376
+ </xsl:template>
377
+ </xsl:stylesheet>
378
+ EOF
379
+
380
+ INDENT_XSLT = Nokogiri::XSLT(INDENT_XSLT_STRING.gsub('__INDENT_STRING__', ' '))
381
+
382
+
383
+ =end
@@ -0,0 +1,158 @@
1
+ require 'haml'
2
+ require 'tilt'
3
+ require 'RedCloth'
4
+ require 'rdiscount'
5
+
6
+ module Amber
7
+ module Render
8
+ class Template
9
+
10
+ PROPERTY_HEADER = /^\s*(^(|- )@\w[^\n]*?\n)*/m
11
+
12
+ RENDER_MAP = {
13
+ :text => 'render_textile',
14
+ :textile => 'render_textile',
15
+ :md => 'render_markdown',
16
+ :markdown => 'render_markdown',
17
+ :html => 'render_raw'
18
+ }
19
+
20
+ TEXTILE_TOC_RE = /^\s*h([1-6])\.\s+(.*)/
21
+
22
+ ERB_TAG_RE = /<%=.*?%>/
23
+ ERB_PLACEHOLDER_RE = /xx erb tag\d+ xx/
24
+
25
+ attr_reader :file
26
+ attr_reader :type
27
+ attr_reader :content
28
+ attr_reader :partial
29
+
30
+ def initialize(options={})
31
+ if options[:file]
32
+ @file = options[:file]
33
+ @type = type_from_file(@file)
34
+ elsif options[:content]
35
+ @content = options[:content]
36
+ @type = options[:type] # e.g. :haml. required if @content
37
+ end
38
+ @partial = options[:partial]
39
+ end
40
+
41
+ #
42
+ # returns rendered content or title, depending on render_mode
43
+ #
44
+ def render(view, options={})
45
+ view.locals[:_type] = @type
46
+ render_mode = options.delete(:mode) || :content
47
+ toc = options.delete(:toc)
48
+
49
+ if render_mode == :title
50
+ render_title(view)
51
+ else
52
+ html = render_html(view)
53
+ if render_mode == :toc
54
+ RegexTableOfContents.new(html, options).to_toc
55
+ elsif toc || render_mode == :toc_and_content
56
+ toc = RegexTableOfContents.new(html, options)
57
+ %(<div id="TOC">%s</div>\n\n%s) % [toc.to_toc, toc.to_html]
58
+ else
59
+ html
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def render_html(view)
67
+ if @type == :haml
68
+ return render_haml(@file, view)
69
+ else
70
+ @content ||= File.read(@file, :encoding => 'UTF-8').sub(PROPERTY_HEADER, '') # remove property header
71
+ if method = RENDER_MAP[@type]
72
+ content, erb_tags = replace_erb_tags(@content)
73
+ html = self.send(method, view, content)
74
+ return render_erb(restore_erb_tags(html, erb_tags), view)
75
+ else
76
+ return "sorry, i don't understand how to render `#{@type}`"
77
+ end
78
+ end
79
+ end
80
+
81
+ def render_erb(string, view)
82
+ template = Tilt::ERBTemplate.new {string}
83
+ template.render(view)
84
+ end
85
+
86
+ #
87
+ # takes raw markup, and replaces every <%= x %> with a
88
+ # markup-safe placeholder. erb_tags holds a map of placeholder
89
+ # to original erb. e.g. {"ERBTAG0" => "<%= 'hi]]"}
90
+ #
91
+ def replace_erb_tags(content)
92
+ counter = 0
93
+ erb_tags = {}
94
+ new_content = content.gsub(ERB_TAG_RE) do |match|
95
+ placeholder = "xx erb tag#{counter} xx"
96
+ erb_tags[placeholder] = match
97
+ counter+=1
98
+ placeholder
99
+ end
100
+ return [new_content, erb_tags]
101
+ end
102
+
103
+ #
104
+ # replaces erb placeholders with actual erb
105
+ #
106
+ def restore_erb_tags(html, erb_tags)
107
+ html.gsub(ERB_PLACEHOLDER_RE) do |match|
108
+ erb_tags[match]
109
+ end
110
+ end
111
+
112
+ def render_title(view)
113
+ locale = view.locals[:locale]
114
+ if title = view.page.explicit_title(locale)
115
+ "<h1>#{title}</h1>\n"
116
+ else
117
+ ""
118
+ end
119
+ end
120
+
121
+ def render_haml(file_path, view)
122
+ template = Tilt::HamlTemplate.new(file_path, {:format => :html5, :default_encoding => 'UTF-8'})
123
+ add_bracket_links(view, template.render(view))
124
+ end
125
+
126
+ def render_textile(view, content)
127
+ content = add_bracket_links(view, content)
128
+ Autolink.auto_link(RedCloth.new(content).to_html)
129
+ end
130
+
131
+ def render_markdown(view, content)
132
+ content = add_bracket_links(view, content)
133
+ RDiscount.new(content, :smart, :autolink).to_html
134
+ end
135
+
136
+ def render_raw(view, content)
137
+ add_bracket_links(view, content)
138
+ end
139
+
140
+ def add_bracket_links(view, content)
141
+ content = Bracketlink.bracket_link(content) do |from, to|
142
+ view.link({from => to})
143
+ end
144
+ content
145
+ end
146
+
147
+ def type_from_file(file_path)
148
+ suffix = File.extname(file_path)
149
+ if suffix
150
+ suffix.sub! /^\./, ''
151
+ suffix = suffix.to_sym
152
+ end
153
+ suffix
154
+ end
155
+
156
+ end
157
+ end
158
+ end