ulysses 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 73480e671295c5f65cf31c8cefcdd7a8540756d2
4
- data.tar.gz: edb7c0a25b78f238d7d3e4bc010618093de14e60
3
+ metadata.gz: 3c7b1d519c285491fa42fda0e4f870252021883b
4
+ data.tar.gz: 38235905aa64dc7ead038ec120bbfb0daaf116c3
5
5
  SHA512:
6
- metadata.gz: 6a4c39908d0e70c7274bc05225b864d459b38f86458fca1f6562e3e1313b3353f20da98390fc5b4b264259e292f67ba65f9684bab94a84e0d0ea818527851d65
7
- data.tar.gz: 7edb67f0772a0e5c9565e8c52b82022f9ce6794ad4df318da83ff1081489db62591e9aec97ba4cd683191ab90efb34589200ea57100a452b4749e878926c61fa
6
+ metadata.gz: 4ea24651011856c59aa05c7a6d6129b2c256c7b60b42d0a117d77273b772f044f85b32d7686549a6436383f57d2540c1061bd3a0f83bf3dda1450643de902e05
7
+ data.tar.gz: b64626d3d96e51b58d2e4be8fd58ea6c3ab081fb9f1ad44e9055c69d91a16086cde221d77b86882b7cec816b02dd2b1baa8f40fd2228cce961456a9ec30f9072
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Ulysses
2
2
 
3
+ [![Join the chat at https://gitter.im/yaodong/gem-ulysses](https://badges.gitter.im/yaodong/gem-ulysses.svg)](https://gitter.im/yaodong/gem-ulysses?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Code Climate](https://codeclimate.com/github/yaodong/gem-ulysses/badges/gpa.svg)](https://codeclimate.com/github/yaodong/gem-ulysses)
4
+
3
5
  This is a library to export your to HTML files. It still in development.
4
6
 
5
7
  ## Installation
@@ -4,7 +4,8 @@ require 'kramdown'
4
4
  require 'ulysses/library'
5
5
  require 'ulysses/group'
6
6
  require 'ulysses/sheet'
7
- require 'ulysses/exporter'
7
+ require 'ulysses/printer'
8
+ require 'ulysses/printer/elements'
8
9
  require 'ulysses/version'
9
10
 
10
11
  module Ulysses
@@ -4,41 +4,58 @@ module Ulysses
4
4
  attr_reader :dirname
5
5
 
6
6
  def initialize(info_file_path)
7
- @dirname = File.dirname(info_file_path)
8
- @info = parse_info(info_file_path)
7
+ @info_file = info_file_path
8
+ @dirname = File.dirname(@info_file)
9
+ end
10
+
11
+ def info
12
+ @info ||= parse_info
9
13
  end
10
14
 
11
15
  def display_name
12
- @display_name = @info['displayName'].content
16
+ @display_name ||= info['displayName'].content
13
17
  end
14
18
 
15
19
  def children
16
- return [] if @info['childOrder'].nil?
17
- @info['childOrder'].children.map do |child|
18
- Group.new File.join(@dirname, child.content, 'Info.ulgroup') if child.element?
19
- end.compact
20
+ @children ||= parse_children
20
21
  end
22
+ alias :groups :children
21
23
 
22
24
  def sheets
23
- list = @info['sheetClusters'].children.select { |c| c.element? && c.name == 'array' }
24
- list = list.map do |i|
25
- content_node = i.children.find { |c| c.element? && c.name == 'string' }
26
- content_node.content
27
- end
28
- list.map do |dirname|
29
- Sheet.new File.join(@dirname, dirname)
30
- end
25
+ @sheets ||= parse_sheets
26
+ end
27
+
28
+ def reload
29
+ @info, @children, @sheets = nil
31
30
  end
32
31
 
33
32
  private
34
33
 
35
- def parse_info(file_path)
36
- xml = Nokogiri::XML File.read(file_path)
37
- dict = xml.xpath('//dict').children.map do |child|
38
- (child.name == 'key' ? child.content : child) if child.element?
39
- end.compact
34
+ def parse_info
35
+ xml = Nokogiri::XML File.read(@info_file)
36
+ dict = xml.xpath('//dict')
37
+ .children
38
+ .select { |child| child.element? }
39
+ .map { |child| child.name == 'key' ? child.content : child }
40
40
  Hash[*dict]
41
41
  end
42
42
 
43
+ def parse_sheets
44
+ return [] unless info['sheetClusters']
45
+ info['sheetClusters']
46
+ .children
47
+ .select { |c| c.element? && c.name == 'array' }
48
+ .map { |i| i.children.find { |c| c.element? && c.name == 'string' }.content }
49
+ .map { |dir| Sheet.new File.join(@dirname, dir) }
50
+ end
51
+
52
+ def parse_children
53
+ return [] unless info['childOrder']
54
+ info['childOrder']
55
+ .children
56
+ .select { |child| child.element? }
57
+ .map { |child| Group.new File.join(@dirname, child.content, 'Info.ulgroup') }
58
+ end
59
+
43
60
  end
44
61
  end
@@ -1,14 +1,25 @@
1
1
  module Ulysses
2
2
  class Library
3
3
 
4
+ DEFAULT_LIBRARY_DIR = '~/Library/Mobile Documents/X5AZV975AG~com~soulmen~ulysses3/Documents/Library'
5
+
4
6
  attr_reader :dirname
5
7
 
6
8
  def initialize(dirname = nil)
7
- dirname ||= '~/Library/Mobile Documents/X5AZV975AG~com~soulmen~ulysses3/Documents/Library'
8
- @dirname = File.expand_path(dirname)
9
+ @dirname = File.expand_path(dirname ||= DEFAULT_LIBRARY_DIR)
9
10
  end
10
11
 
11
12
  def groups
13
+ @groups ||= parse_groups
14
+ end
15
+
16
+ def reload
17
+ @groups = nil
18
+ end
19
+
20
+ private
21
+
22
+ def parse_groups
12
23
  Dir.glob(File.join @dirname, 'Groups-ulgroup', '*.ulgroup').map do |info_file|
13
24
  Group.new info_file
14
25
  end
@@ -0,0 +1,108 @@
1
+ module Ulysses
2
+ class Printer
3
+
4
+ SHEET_CONTENT_XPATH = '/sheet/string[@xml:space="preserve"]'
5
+
6
+ def initialize(target)
7
+ @target = target
8
+ @footnotes = []
9
+ @annotations = []
10
+ @html_entities = HTMLEntities.new
11
+ end
12
+
13
+ def print
14
+ if @target.is_a? Library
15
+ print_library(@target)
16
+ elsif @target.is_a? Group
17
+ print_group(@target)
18
+ elsif @target.is_a? Sheet
19
+ print_sheet(@target)
20
+ else
21
+ raise "Unsupported print type: #{@target.class}"
22
+ end
23
+ end
24
+
25
+ def footnotes
26
+ @footnotes
27
+ end
28
+
29
+ def annotations
30
+ @annotations
31
+ end
32
+
33
+ private
34
+
35
+ def print_library(library)
36
+ library.groups.map { |g| print_group(g) }.join("\n")
37
+ end
38
+
39
+ def print_group(group)
40
+ group.sheets.map { |s| print_sheet(s) }.join("\n") + group.groups.map { |g| print_group(g) }.join("\n")
41
+ end
42
+
43
+ def print_sheet(sheet)
44
+ paragraphs = sheet.xml.xpath(SHEET_CONTENT_XPATH).children.select { |n| n.element? }
45
+ paragraphs.map{ |p| print_paragraph(p) }.join("\n")
46
+ end
47
+
48
+ def print_paragraph(p)
49
+ children = p.children
50
+ tags = (children.any? && children.first.name === 'tags') ? parse_tags(children.shift) : []
51
+ content = parse_content(children)
52
+
53
+ tabs = tags.count('tab')
54
+ if tabs > 0
55
+ tags.delete('tab')
56
+ tags << "tabs_#{tabs}"
57
+ end
58
+ tags = tags.uniq.map{|t| normalize_tag(t)}.join(' ')
59
+ "<p class=\"#{tags}\">#{content}</p>"
60
+ end
61
+
62
+ def parse_content(nodes)
63
+ nodes.map do |node|
64
+ if node.text?
65
+ node.content
66
+ else
67
+ send("parse_#{node.name}", node)
68
+ end
69
+ end.join
70
+ end
71
+
72
+ def parse_tags(tags)
73
+ tags.children.map do |tag|
74
+ if tag.attributes.has_key? 'kind'
75
+ tag.attributes['kind'].value
76
+ elsif tag.content === "\t"
77
+ 'tab'
78
+ end
79
+ end
80
+ end
81
+
82
+ def parse_escape(node)
83
+ node.content.gsub /\\(.)/, '\1'
84
+ end
85
+
86
+ def parse_p(node)
87
+ '<p>' + parse_content(node.children) + '</p>'
88
+ end
89
+
90
+ def parse_string(node)
91
+ parse_content node.children
92
+ end
93
+
94
+ def parse_element(node)
95
+ send "parse_element_#{node.attributes['kind'].value}", node
96
+ end
97
+
98
+ def normalize_tag(tag)
99
+ tag.gsub(/::/, '/')
100
+ .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
101
+ .gsub(/([a-z\d])([A-Z])/,'\1_\2')
102
+ .gsub(/([a-z])(\d)/i, '\1_\2')
103
+ .tr('-', '_')
104
+ .downcase
105
+ end
106
+
107
+ end
108
+ end
@@ -0,0 +1,69 @@
1
+ module Ulysses
2
+ class Printer
3
+
4
+ def parse_element_strong(node)
5
+ '<strong>' + node.content + '</strong>'
6
+ end
7
+
8
+ def parse_element_emph(node)
9
+ '<em>' + node.content + '</em>'
10
+ end
11
+
12
+ def parse_element_mark(node)
13
+ '<span class="marked">' + node.content + '</span>'
14
+ end
15
+
16
+ def parse_element_delete(node)
17
+ '<del>' + node.content + '</del>'
18
+ end
19
+
20
+ def parse_element_inlineComment(node)
21
+ '<span class="inline-comment">' + node.content + '</span>'
22
+ end
23
+
24
+ def parse_element_code(node)
25
+ '<code>' + node.content + '</code>'
26
+ end
27
+
28
+ def parse_element_inlineNative(node)
29
+ '<span class="inline-native">' + node.content + '</span>'
30
+ end
31
+
32
+ def parse_element_link(node)
33
+ attrs = parse_element_attributes(node)
34
+ content = parse_content(node.children.select{ |child| !child.element? || child.name != 'attribute' })
35
+ '<a href="' + attrs.fetch('URL', '') + '" title="' + attrs.fetch('title', '') + '">' + content + '</a>'
36
+ end
37
+
38
+ def parse_element_image(node)
39
+ attrs = parse_element_attributes(node)
40
+ '<img src="' + attrs.fetch('URL', '') + '" alt="' + attrs.fetch('title', '') + '" />'
41
+ end
42
+
43
+ def parse_element_video(node)
44
+ attrs = parse_element_attributes(node)
45
+ '<video><source src="' + attrs['URL'] + '" /></video>'
46
+ end
47
+
48
+ def parse_element_footnote(node)
49
+ attrs = parse_element_attributes(node)
50
+ @footnotes << attrs['text']
51
+ '<sup class="footnote-ref"><a href="#fn' + @footnotes.size.to_s + '">' + @footnotes.size.to_s + '</a></sup>'
52
+ end
53
+
54
+ def parse_element_annotation(node)
55
+ attrs = parse_element_attributes(node)
56
+ @annotations << attrs['text']
57
+ '<span class="annotation" data-id="' + @annotations.size.to_s + '">' + @annotations.size.to_s + '</span>'
58
+ end
59
+
60
+ def parse_element_attributes(element)
61
+ attributes = element.children.select { |child| child.element? && child.name === 'attribute' }
62
+ attributes.map! do |attr|
63
+ [attr.attributes['identifier'].value, parse_content(attr.children)]
64
+ end
65
+ Hash[attributes]
66
+ end
67
+
68
+ end
69
+ end
@@ -7,21 +7,39 @@ module Ulysses
7
7
  @dirname = dirname
8
8
  end
9
9
 
10
- def reload
11
- @content = nil
12
- @text = nil
10
+ def markup
11
+ @markup ||= parse_markup
13
12
  end
14
13
 
15
- def content
16
- @content ||= File.read(File.join(@dirname, 'Content.xml'))
14
+ def xml
15
+ @xml ||= Nokogiri::XML(File.read(File.join(@dirname, 'Content.xml')))
17
16
  end
18
17
 
19
- def text
20
- @text ||= File.read(File.join(@dirname, 'Text.txt'))
18
+ def to_html
19
+ @html ||= Exporter.new(xml).to_html
21
20
  end
22
21
 
23
- def to_html
24
- Exporter.new(content).to_html
22
+ def reload
23
+ @markup, @xml, @html = nil
24
+ end
25
+
26
+ private
27
+
28
+ def parse_xml_attributes(node)
29
+ Hash[node.attributes.map { |nm, el| [nm.to_sym, el.value] }]
30
+ end
31
+
32
+ def parse_markup
33
+ segment = xml.xpath('/sheet/markup')[0]
34
+ markup = parse_xml_attributes(segment)
35
+ markup[:definitions] = begin
36
+ defines = segment.children.select { |node| node.element? }.map do |node|
37
+ attrs = parse_xml_attributes(node)
38
+ [attrs[:definition].to_sym, attrs]
39
+ end
40
+ Hash[defines]
41
+ end
42
+ markup
25
43
  end
26
44
 
27
45
  end
@@ -1,3 +1,3 @@
1
1
  module Ulysses
2
- VERSION = '0.3.3'
2
+ VERSION = '0.4.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ulysses
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaodong Zhao
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-02-06 00:00:00.000000000 Z
11
+ date: 2016-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -112,9 +112,10 @@ files:
112
112
  - bin/console
113
113
  - bin/setup
114
114
  - lib/ulysses.rb
115
- - lib/ulysses/exporter.rb
116
115
  - lib/ulysses/group.rb
117
116
  - lib/ulysses/library.rb
117
+ - lib/ulysses/printer.rb
118
+ - lib/ulysses/printer/elements.rb
118
119
  - lib/ulysses/sheet.rb
119
120
  - lib/ulysses/version.rb
120
121
  - ulysses.gemspec
@@ -1,287 +0,0 @@
1
- module Ulysses
2
- class Exporter
3
-
4
- def initialize(sheet_xml)
5
- @xml = Nokogiri::XML sheet_xml
6
- @coder = HTMLEntities.new
7
- @annotations = []
8
- @footnotes = []
9
- end
10
-
11
- def to_html
12
- tree = xml_to_tree @xml.xpath('/sheet/string[@xml:space="preserve"]')
13
- html = tree_to_html tree
14
- html = parse_prefix_tags(html)
15
- html = append_footnotes(html)
16
- html = append_annotations(html)
17
- Kramdown::Document.new(html).to_html
18
- end
19
-
20
- private
21
-
22
- def xml_to_tree(xml)
23
- xml.children.map do |child|
24
- if child.text?
25
- child.content
26
- elsif child.element?
27
- {
28
- name: child.name,
29
- attributes: Hash[child.attribute_nodes.map { |an| [an.node_name, an.content] }],
30
- children: child.children.length > 0 ? xml_to_tree(child) : []
31
- }
32
- else
33
- raise "Unknown node type: #{child.class}"
34
- end
35
- end
36
- end
37
-
38
- def tree_to_html(tree)
39
- html = ''
40
- tree.each do |node|
41
- if node.is_a?(String)
42
- html += @coder.encode node
43
- else
44
- case node[:name]
45
- when 'p'
46
- html += p_to_html node
47
- when 'tags'
48
- html += tree_to_html(node[:children])
49
- when 'tag'
50
- html += prefix_tag_to_placeholder node
51
- when 'element'
52
- html += element_to_html(node)
53
- when 'attribute'
54
- html += attribute_to_html(node)
55
- when 'string'
56
- html += string_to_html(node)
57
- else
58
- raise "Unknown tree node type: #{node[:name]}"
59
- end
60
- end
61
- end
62
- html
63
- end
64
-
65
- def p_to_html(node)
66
- if node[:children].any?
67
- tree_to_html(node[:children])
68
- else
69
- ''
70
- end
71
- end
72
-
73
- def inline_tag_to_html(node, tag, attr = nil)
74
- open_tag = attr.nil? ? "<#{tag} #{attr}>" : "<#{tag}>"
75
- if node[:children].any?
76
- open_tag + tree_to_html(node[:children]) + "</#{tag}>"
77
- else
78
- "#{open_tag}</#{tag}>"
79
- end
80
- end
81
-
82
- def link_to_html(link)
83
- string = '<a'
84
- text = ''
85
- link[:children].each do |child|
86
- if child.is_a? String
87
- text = child
88
- else
89
- identifier = child[:attributes]['identifier']
90
- case
91
- when 'URL'
92
- string += ' url="'+ child[:children].first + '"'
93
- when 'title'
94
- string += ' title="'+ child[:children].first + '"'
95
- else
96
- raise "unknown link attr identifier #{identifier}"
97
- end
98
- end
99
- end
100
- string + '>' + text + '</a>'
101
- end
102
-
103
- def image_to_html(node)
104
- html = '<img'
105
- node[:children].each do |child|
106
- case child[:attributes]['identifier']
107
- when 'URL'
108
- html += ' url="'+ child[:children].first + '"'
109
- when 'title'
110
- html += ' title="'+ child[:children].first + '"'
111
- else
112
- # skip
113
- end
114
- end
115
- html + ' />'
116
- end
117
-
118
- def video_to_html(node)
119
- source = ''
120
- node[:children].each do |child|
121
- case child[:attributes]['identifier']
122
- when 'URL'
123
- source = child[:children].first
124
- else
125
- # skip
126
- end
127
- end
128
- '<video><source src="' + source + '"></video>'
129
- end
130
-
131
- def element_to_html(node)
132
- case node[:attributes]['kind']
133
- when 'strong'
134
- inline_tag_to_html(node, 'strong')
135
- when 'emph'
136
- inline_tag_to_html(node, 'em')
137
- when 'mark'
138
- inline_tag_to_html(node, 'span', 'class="marked"')
139
- when 'delete'
140
- inline_tag_to_html(node, 'del')
141
- when 'inlineComment'
142
- inline_tag_to_html(node, 'span', 'class="comment"')
143
- when 'code'
144
- inline_tag_to_html(node, 'code')
145
- when 'inlineNative'
146
- inline_tag_to_html(node, 'span', 'class="native"')
147
- when 'link'
148
- link_to_html(node)
149
- when 'annotation'
150
- @annotations << [node[:children].last, tree_to_html(node[:children].first[:children])]
151
- "<placeholder-annotation-#{@annotations.size - 1}/>"
152
- when 'image'
153
- image_to_html(node)
154
- when 'video'
155
- video_to_html(node)
156
- when 'footnote'
157
- string_node = node[:children].first[:children].first
158
- @footnotes << tree_to_html(string_node[:children])
159
- "<placeholder-footnote-#{@footnotes.size - 1}/>"
160
- else
161
- raise node
162
- end
163
- end
164
-
165
- def prefix_tag_to_placeholder(node)
166
- case node[:attributes]['kind']
167
- when 'codeblock'
168
- string = '<<prefix-tag-code-block>>'
169
- when 'comment'
170
- string = '<<prefix-tag-comment>>'
171
- when 'divider'
172
- string = '<hr class="divider" />'
173
- when 'nativeblock'
174
- string = '<<prefix-tag-native-block>>'
175
- when 'blockquote'
176
- string = '<<prefix-tag-block-quote>>'
177
- when 'orderedList'
178
- string = node[:children].first
179
- when 'unorderedList'
180
- string = node[:children].first
181
- when 'heading1'
182
- string = '<<prefix-tag-heading-1>>'
183
- when 'heading2'
184
- string = '<<prefix-tag-heading-2>>'
185
- when 'heading3'
186
- string = '<<prefix-tag-heading-5>>'
187
- when 'heading4'
188
- string = '<<prefix-tag-heading-4>>'
189
- when 'heading5'
190
- string = '<<prefix-tag-heading-5>>'
191
- when 'heading6'
192
- string = '<<prefix-tag-heading-6>>'
193
- else
194
- if node[:attributes].empty? && node[:children].first == "\t"
195
- string = "\t"
196
- else
197
- raise node
198
- end
199
- end
200
- string
201
- end
202
-
203
- def attribute_to_html(node)
204
- case node[:attributes]['identifier']
205
- when 'text'
206
- html = tree_to_html node[:children]
207
- else
208
- raise "Unknown attribute node type: #{node[:attributes]['identifier']}"
209
- end
210
- html
211
- end
212
-
213
- def string_to_html(node)
214
- case node[:attributes]['space']
215
- when 'preserve'
216
- html = tree_to_html node[:children]
217
- else
218
- raise "Unknown string node: #{node[:attributes]['space']}"
219
- end
220
- html
221
- end
222
-
223
- def parse_prefix_tags(html)
224
- lines = html.split("\n")
225
-
226
- prefix_tags = []
227
- lines = lines.map do |line|
228
- if /\A<<(prefix-tag[a-z0-9\-]+)>>(.*)\Z/i.match line
229
- prefix_tags << $1
230
- "<#{$1}>" + $2 + "</#{$1}>"
231
- else
232
- line
233
- end
234
- end
235
-
236
- html = lines.join("\n")
237
- prefix_tags.uniq.each do |prefix|
238
- case prefix
239
- when 'prefix-tag-code-block'
240
- html_tag = 'pre-code'
241
- when 'prefix-tag-native-block'
242
- html_tag = 'pre-raw'
243
- when 'prefix-tag-comment'
244
- html_tag = 'should-delete'
245
- when 'prefix-tag-block-quote'
246
- html_tag = 'blockquote'
247
- when /\Aprefix-tag-heading-(\d)\Z/i
248
- html_tag = "h#{$1}"
249
- else
250
- raise "Unknown prefix tag: #{prefix}"
251
- end
252
- html.gsub! %r/(<\/?)#{prefix}>/, "\\1#{html_tag}>"
253
- html.gsub! %r/<\/#{html_tag}>(\n*)<#{html_tag}>/, "\\1"
254
- end
255
-
256
- html.gsub! /<should-delete>.*<\/should-delete>\n?/, ''
257
-
258
- html.gsub! /<pre-code>/, '<pre><code>'
259
- html.gsub! /<\/pre-code>/, '</code></pre>'
260
-
261
- html.gsub! /<pre-raw>/, '<p class="raw">'
262
- html.gsub! /<\/pre-raw>/, '</p>'
263
-
264
- html
265
- end
266
-
267
- def append_footnotes(html)
268
- footnote_html = '<div class="footnotes">'
269
- @footnotes.each_with_index do |fn, index|
270
- html.gsub! /<placeholder-footnote-#{index}\/>/, "<sup><a href=\"#fn#{index}\" id=\"ref#{index}\">#{index}</a></sup>"
271
- footnote_html += "<sup id=\"fn#{index}\">#{index}. " + fn + '</sup>'
272
- end
273
- html + "\n\n" + footnote_html + "</div>\n"
274
- end
275
-
276
- def append_annotations(html)
277
- annotations_html = '<div class="annotations">'
278
- @annotations.each_with_index do |at, index|
279
- html.gsub! /<placeholder-annotation-#{index}/, "<span class=\"annotated\" data-annotation=\"#{index}\">#{at[0]}</span>"
280
- annotations_html += "<section data-annotation=\"#{index}\">" + at[1] + '</section>'
281
- end
282
-
283
- html + "\n\n" + annotations_html + "</div>\n"
284
- end
285
-
286
- end
287
- end