tartancloth 0.0.1 → 0.0.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/.rspec +2 -0
- data/CHANGELOG.txt +9 -0
- data/README.md +237 -200
- data/lib/tartancloth.rb +417 -335
- data/spec/lib/matchers.rb +110 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/toc_spec.rb +37 -0
- metadata +11 -3
data/lib/tartancloth.rb
CHANGED
@@ -1,335 +1,417 @@
|
|
1
|
-
require 'bluecloth'
|
2
|
-
require 'nokogiri'
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
class TartanCloth
|
7
|
-
|
8
|
-
VERSION = "0.0.
|
9
|
-
|
10
|
-
attr_accessor :title
|
11
|
-
|
12
|
-
def initialize( markdown_file, title = nil )
|
13
|
-
@markdown_file = markdown_file
|
14
|
-
@title = title
|
15
|
-
end
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
#
|
83
|
-
|
84
|
-
|
85
|
-
#
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
#
|
92
|
-
|
93
|
-
|
94
|
-
#
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
end
|
113
|
-
|
114
|
-
###
|
115
|
-
#
|
116
|
-
#
|
117
|
-
#
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
# returns
|
146
|
-
def
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
}
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
}
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
1
|
+
require 'bluecloth'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
class TartanCloth
|
7
|
+
|
8
|
+
VERSION = "0.0.2"
|
9
|
+
|
10
|
+
attr_accessor :title
|
11
|
+
|
12
|
+
def initialize( markdown_file, title = nil )
|
13
|
+
@markdown_file = markdown_file
|
14
|
+
@title = title
|
15
|
+
end
|
16
|
+
|
17
|
+
###
|
18
|
+
# Convert a markdown source file to HTML. If a header element with text TOC
|
19
|
+
# exists within the markdown document, a Table of Contents will be generated
|
20
|
+
# and inserted at that location.
|
21
|
+
#
|
22
|
+
# The TOC will only contain header (h1-h6) elements from the location of the
|
23
|
+
# TOC header to the end of the document
|
24
|
+
def to_html
|
25
|
+
html = ""
|
26
|
+
|
27
|
+
# Add a well formed HTML5 header.
|
28
|
+
html << html_header(title)
|
29
|
+
|
30
|
+
# Add the body content.
|
31
|
+
html << body_html()
|
32
|
+
|
33
|
+
# Add the document closing tags.
|
34
|
+
html << html_footer()
|
35
|
+
end
|
36
|
+
|
37
|
+
###
|
38
|
+
# The same as to_html() but writes the HTML to a file.
|
39
|
+
#
|
40
|
+
# html_file - path to file
|
41
|
+
def to_html_file( html_file )
|
42
|
+
|
43
|
+
File.open( html_file, 'w') do |f|
|
44
|
+
f << to_html()
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
###
|
49
|
+
# Build TOC and return body content (including TOC).
|
50
|
+
# Returned HTML does NOT include doc headers, footer, or stylesheet.
|
51
|
+
#
|
52
|
+
# returns HTML that forms the body of the document
|
53
|
+
def body_html
|
54
|
+
bc = BlueCloth::new( File::read( @markdown_file ), header_labels: true )
|
55
|
+
body = bc.to_html
|
56
|
+
|
57
|
+
body = build_toc( body )
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
###
|
63
|
+
# Build a TOC based on headers located within HTML content.
|
64
|
+
# If a header element with text TOC exists within the markdown document, a
|
65
|
+
# Table of Contents will be generated and inserted at that location.
|
66
|
+
#
|
67
|
+
# The TOC will only contain header (h1-h6) elements from the location of the
|
68
|
+
# TOC header to the end of the document
|
69
|
+
def build_toc( html_content )
|
70
|
+
# Generate Nokogiri elements from HTML
|
71
|
+
doc = Nokogiri::HTML::DocumentFragment.parse( html_content )
|
72
|
+
|
73
|
+
# Make sure all header anchors are unique.
|
74
|
+
make_header_anchors_unique(doc)
|
75
|
+
|
76
|
+
# Find the TOC header
|
77
|
+
toc = find_toc_header(doc)
|
78
|
+
|
79
|
+
# Just return what was passed to us if there's no TOC.
|
80
|
+
return html_content if toc.nil?
|
81
|
+
|
82
|
+
# Get all headers in the document, starting from the TOC header.
|
83
|
+
headers = get_headers(doc, toc)
|
84
|
+
|
85
|
+
# Build the link info for the TOC.
|
86
|
+
toc_links = []
|
87
|
+
headers.each do |element|
|
88
|
+
toc_links << link_hash(element)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Convert link info to markdown.
|
92
|
+
toc_md = toc_to_markdown(toc_links)
|
93
|
+
|
94
|
+
# Convert the TOC markdown to HTML
|
95
|
+
bc = BlueCloth::new( toc_md, pseudoprotocols: true )
|
96
|
+
toc_content = bc.to_html
|
97
|
+
|
98
|
+
# Convert the TOC HTML to Nokogiri elements.
|
99
|
+
toc_html = Nokogiri::HTML::DocumentFragment.parse(toc_content)
|
100
|
+
|
101
|
+
# Add toc class to the <ul> element
|
102
|
+
toc_html.css('ul').add_class('toc')
|
103
|
+
|
104
|
+
# Insert the TOC content before the toc element
|
105
|
+
toc.before(toc_html.children)
|
106
|
+
|
107
|
+
# Remove the TOC header placeholder element.
|
108
|
+
toc.remove
|
109
|
+
|
110
|
+
# Return the HTML
|
111
|
+
doc.to_html
|
112
|
+
end
|
113
|
+
|
114
|
+
###
|
115
|
+
# Convert an array of link hashes to markdown
|
116
|
+
#
|
117
|
+
# toc_links - hash of links
|
118
|
+
# returns - markdown content
|
119
|
+
def toc_to_markdown(toc_links)
|
120
|
+
markdown = "## Table of Contents\n\n"
|
121
|
+
toc_links.each do |link_data|
|
122
|
+
text = link_data[:text]
|
123
|
+
link = link_data[:link]
|
124
|
+
klass = link_data[:klass]
|
125
|
+
markdown << "+ [[#{text}](##{link})](class:#{klass})\n"
|
126
|
+
end
|
127
|
+
markdown << "\n"
|
128
|
+
end
|
129
|
+
|
130
|
+
###
|
131
|
+
# return the TOC header element or nil
|
132
|
+
#
|
133
|
+
# doc - Nokogiri DocumentFragment
|
134
|
+
def find_toc_header(doc)
|
135
|
+
return nil unless doc
|
136
|
+
|
137
|
+
doc.children.each do |element|
|
138
|
+
return element if is_toc_header(element)
|
139
|
+
end
|
140
|
+
|
141
|
+
return nil
|
142
|
+
end
|
143
|
+
|
144
|
+
###
|
145
|
+
# returns true if the element is a header (h1-h6) element
|
146
|
+
def is_header_element(element)
|
147
|
+
%w(h1 h2 h3 h4 h5 h6).include? element.name
|
148
|
+
end
|
149
|
+
|
150
|
+
###
|
151
|
+
# returns true when a header (h1-h6) element contains the text: TOC
|
152
|
+
def is_toc_header(element)
|
153
|
+
return (is_header_element(element) && element.text == 'TOC')
|
154
|
+
end
|
155
|
+
|
156
|
+
###
|
157
|
+
# Create an array of all header (h1-h6) elements in an HTML document
|
158
|
+
# starting from a specific element
|
159
|
+
#
|
160
|
+
# starting_element - element to start parsing from, if starting element is
|
161
|
+
# nil, all headers will be collected.
|
162
|
+
# returns - array of Nokogiri elements
|
163
|
+
def get_headers(doc, starting_element = nil)
|
164
|
+
headers = []
|
165
|
+
capture = (starting_element.nil? ? true : false)
|
166
|
+
|
167
|
+
doc.children.each do |element|
|
168
|
+
unless capture
|
169
|
+
capture = true if element == starting_element
|
170
|
+
next
|
171
|
+
end # unless
|
172
|
+
|
173
|
+
headers << element if is_header_element(element)
|
174
|
+
end
|
175
|
+
|
176
|
+
headers
|
177
|
+
end
|
178
|
+
|
179
|
+
###
|
180
|
+
# Build a link hash for an element containing the text, link,
|
181
|
+
# and a children array.
|
182
|
+
#
|
183
|
+
# element - Nokogiri element
|
184
|
+
def link_hash(element)
|
185
|
+
anchor = get_anchor_for_header(element)
|
186
|
+
anchor_link = get_link(anchor)
|
187
|
+
|
188
|
+
# Store the header text (link text) and the anchor link and a class for styling.
|
189
|
+
{ text: element.text, link: anchor_link, klass: "#{element.name}toc" }
|
190
|
+
end
|
191
|
+
|
192
|
+
###
|
193
|
+
# Return the previous element which should be an anchor
|
194
|
+
def get_anchor_for_header(element)
|
195
|
+
# The previous element should be a simple anchor.
|
196
|
+
# Get the actual link value from the anchor.
|
197
|
+
anchor = element.previous_element
|
198
|
+
anchor = nil unless anchor.name == 'a'
|
199
|
+
anchor
|
200
|
+
end
|
201
|
+
|
202
|
+
###
|
203
|
+
# Return the link from an element, if it's an anchor
|
204
|
+
# returns "" otherwise
|
205
|
+
#
|
206
|
+
# anchor - Nokogiri::Node
|
207
|
+
def get_link(anchor)
|
208
|
+
link = ""
|
209
|
+
link = anchor.attributes['name'].value if anchor.name == 'a'
|
210
|
+
link
|
211
|
+
end
|
212
|
+
|
213
|
+
###
|
214
|
+
# Sets an anchor's link to a value.
|
215
|
+
# Does nothing if element isn't an anchor.
|
216
|
+
#
|
217
|
+
# anchor - Nokogiri::Node
|
218
|
+
# link - link text to set on anchor
|
219
|
+
def set_link(anchor, link)
|
220
|
+
anchor.attributes['name'].value = link if anchor.name == 'a'
|
221
|
+
end
|
222
|
+
|
223
|
+
###
|
224
|
+
# Identical headers will have identical anchors. Modify them so each anchor
|
225
|
+
# is unique.
|
226
|
+
def make_header_anchors_unique(doc)
|
227
|
+
headers = get_headers(doc)
|
228
|
+
|
229
|
+
# Get anchors for each header.
|
230
|
+
anchors = []
|
231
|
+
headers.each do |h|
|
232
|
+
anchors << get_anchor_for_header(h)
|
233
|
+
end
|
234
|
+
|
235
|
+
anchor_collection = {}
|
236
|
+
anchors.each do |a|
|
237
|
+
# Get the link
|
238
|
+
link = get_link(a)
|
239
|
+
|
240
|
+
# Get the current link count, will be nil if it's the first time.
|
241
|
+
link_count = anchor_collection[link]
|
242
|
+
|
243
|
+
if link_count.nil?
|
244
|
+
# First time we've seen this link
|
245
|
+
link_count = 0
|
246
|
+
|
247
|
+
# Store it in the collection
|
248
|
+
anchor_collection[link] = link_count
|
249
|
+
else
|
250
|
+
# Link already exists, modify it (add .#)
|
251
|
+
set_link(a, "#{link}.#{link_count}")
|
252
|
+
|
253
|
+
# Update the count for the next time we find this link
|
254
|
+
anchor_collection[link] = link_count + 1
|
255
|
+
end # if
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Create an HTML5 header
|
260
|
+
#
|
261
|
+
# returns - HTML header and body open tags
|
262
|
+
def html_header(title)
|
263
|
+
styles = css()
|
264
|
+
header = <<HTML_HEADER
|
265
|
+
<!DOCTYPE html>
|
266
|
+
<html>
|
267
|
+
<head><title>#{title}</title></head>
|
268
|
+
|
269
|
+
#{styles}
|
270
|
+
|
271
|
+
<body>
|
272
|
+
<div class="content">
|
273
|
+
<div class="rendered-content">
|
274
|
+
|
275
|
+
HTML_HEADER
|
276
|
+
end
|
277
|
+
|
278
|
+
###
|
279
|
+
# Create some stylish CSS
|
280
|
+
#
|
281
|
+
# returns - html style element
|
282
|
+
def css()
|
283
|
+
styles = <<CSS
|
284
|
+
<style media="screen" type="text/css">
|
285
|
+
<!--
|
286
|
+
body {
|
287
|
+
font-family: Arial, sans-serif;
|
288
|
+
}
|
289
|
+
|
290
|
+
.content {
|
291
|
+
margin: 0 auto;
|
292
|
+
min-height: 100%;
|
293
|
+
padding: 0 0 100px;
|
294
|
+
width: 980px;
|
295
|
+
border: 1px solid #ccc;
|
296
|
+
border-radius: 5px;
|
297
|
+
}
|
298
|
+
|
299
|
+
.rendered-content {
|
300
|
+
padding: 10px;
|
301
|
+
}
|
302
|
+
|
303
|
+
/* Fancy HR styles based on http://css-tricks.com/examples/hrs/ */
|
304
|
+
hr {
|
305
|
+
border: 0;
|
306
|
+
height: 1px;
|
307
|
+
background-image: -webkit-linear-gradient(left, rgba(200,200,200,1), rgba(200,200,200,0.5), rgba(200,200,200,0));
|
308
|
+
background-image: -moz-linear-gradient(left, rgba(200,200,200,1), rgba(200,200,200,0.5), rgba(200,200,200,0));
|
309
|
+
background-image: -ms-linear-gradient(left, rgba(200,200,200,1), rgba(200,200,200,0.5), rgba(200,200,200,0));
|
310
|
+
background-image: -o-linear-gradient(left, rgba(200,200,200,1), rgba(200,200,200,0.5), rgba(200,200,200,0));
|
311
|
+
}
|
312
|
+
|
313
|
+
h1 {
|
314
|
+
font-size: 24px;
|
315
|
+
font-weight: normal;
|
316
|
+
line-height: 1.25;
|
317
|
+
}
|
318
|
+
|
319
|
+
h2 {
|
320
|
+
font-size: 20px;
|
321
|
+
font-weight: normal;
|
322
|
+
line-height: 1.5;
|
323
|
+
}
|
324
|
+
|
325
|
+
h3 {
|
326
|
+
font-size: 16px;
|
327
|
+
font-weight: bold;
|
328
|
+
line-height: 1.5625;
|
329
|
+
}
|
330
|
+
|
331
|
+
h4 {
|
332
|
+
font-size: 14px;
|
333
|
+
font-weight: bold;
|
334
|
+
line-height: 1.5;
|
335
|
+
}
|
336
|
+
|
337
|
+
h5 {
|
338
|
+
font-size: 12px;
|
339
|
+
font-weight: bold;
|
340
|
+
line-height: 1.66;
|
341
|
+
text-transform: uppercase;
|
342
|
+
}
|
343
|
+
|
344
|
+
h6 {
|
345
|
+
font-size: 12px;
|
346
|
+
font-style: italic;
|
347
|
+
font-weight: bold;
|
348
|
+
line-height: 1.66;
|
349
|
+
text-transform: uppercase;
|
350
|
+
}
|
351
|
+
|
352
|
+
pre {
|
353
|
+
margin-left: 2em;
|
354
|
+
display: block;
|
355
|
+
background: #f5f5f5;
|
356
|
+
font-family: monospace;
|
357
|
+
border: 1px solid #ccc;
|
358
|
+
border-radius: 2px;
|
359
|
+
padding: 1px 3px;
|
360
|
+
}
|
361
|
+
|
362
|
+
code {
|
363
|
+
background: #f5f5f5;
|
364
|
+
font-family: monospace;
|
365
|
+
border: 1px solid #ccc;
|
366
|
+
border-radius: 2px;
|
367
|
+
padding: 1px 3px;
|
368
|
+
}
|
369
|
+
|
370
|
+
pre, code {
|
371
|
+
font-size: 12px;
|
372
|
+
line-height: 1.4;
|
373
|
+
}
|
374
|
+
|
375
|
+
pre code {
|
376
|
+
border: 0;
|
377
|
+
padding: 0;
|
378
|
+
}
|
379
|
+
|
380
|
+
ul.toc li {
|
381
|
+
list-style: none;
|
382
|
+
font-size: 14px;
|
383
|
+
}
|
384
|
+
|
385
|
+
ul.toc li span.h3toc {
|
386
|
+
margin-left: 20px;
|
387
|
+
}
|
388
|
+
|
389
|
+
ul.toc li span.h4toc {
|
390
|
+
margin-left: 40px;
|
391
|
+
}
|
392
|
+
|
393
|
+
ul.toc li span.h5toc {
|
394
|
+
margin-left: 60px;
|
395
|
+
}
|
396
|
+
|
397
|
+
ul.toc li span.h6toc {
|
398
|
+
margin-left: 80px;
|
399
|
+
}
|
400
|
+
-->
|
401
|
+
</style>
|
402
|
+
CSS
|
403
|
+
end
|
404
|
+
|
405
|
+
###
|
406
|
+
# returns - HTML closing tags
|
407
|
+
def html_footer()
|
408
|
+
footer = <<HTML_FOOTER
|
409
|
+
|
410
|
+
</div> <!-- .content -->
|
411
|
+
</div> <!-- .rendered-content -->
|
412
|
+
</body>
|
413
|
+
</html>
|
414
|
+
HTML_FOOTER
|
415
|
+
end
|
416
|
+
|
417
|
+
end
|