ulysses 0.3.3 → 0.4.0

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.
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