repubmark 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +46 -0
  3. data/.rubocop_todo.yml +129 -0
  4. data/.yardopts +4 -0
  5. data/LICENSE +21 -0
  6. data/README.md +21 -0
  7. data/Rakefile +60 -0
  8. data/examples/config.yml +71 -0
  9. data/examples/full.chapters.json +22 -0
  10. data/examples/full.gmi +44 -0
  11. data/examples/full.html +114 -0
  12. data/examples/full.repub +91 -0
  13. data/examples/full.summary.txt +2 -0
  14. data/examples/hello.chapters.json +3 -0
  15. data/examples/hello.gmi +1 -0
  16. data/examples/hello.html +5 -0
  17. data/examples/hello.repub +3 -0
  18. data/examples/hello.summary.txt +1 -0
  19. data/exe/repubmark +46 -0
  20. data/lib/repubmark/config.rb +53 -0
  21. data/lib/repubmark/elems/abbrev.rb +52 -0
  22. data/lib/repubmark/elems/annotation.rb +48 -0
  23. data/lib/repubmark/elems/article.rb +96 -0
  24. data/lib/repubmark/elems/base.rb +104 -0
  25. data/lib/repubmark/elems/blockquote.rb +80 -0
  26. data/lib/repubmark/elems/canvas.rb +104 -0
  27. data/lib/repubmark/elems/caption.rb +69 -0
  28. data/lib/repubmark/elems/chapter.rb +148 -0
  29. data/lib/repubmark/elems/code_block.rb +70 -0
  30. data/lib/repubmark/elems/code_inline.rb +35 -0
  31. data/lib/repubmark/elems/figure.rb +73 -0
  32. data/lib/repubmark/elems/figures.rb +47 -0
  33. data/lib/repubmark/elems/footnote.rb +87 -0
  34. data/lib/repubmark/elems/footnotes_category.rb +48 -0
  35. data/lib/repubmark/elems/fraction.rb +31 -0
  36. data/lib/repubmark/elems/iframe.rb +62 -0
  37. data/lib/repubmark/elems/joint.rb +176 -0
  38. data/lib/repubmark/elems/link.rb +56 -0
  39. data/lib/repubmark/elems/list.rb +84 -0
  40. data/lib/repubmark/elems/list_item.rb +147 -0
  41. data/lib/repubmark/elems/note.rb +40 -0
  42. data/lib/repubmark/elems/paragraph.rb +42 -0
  43. data/lib/repubmark/elems/power.rb +34 -0
  44. data/lib/repubmark/elems/quote.rb +53 -0
  45. data/lib/repubmark/elems/section.rb +58 -0
  46. data/lib/repubmark/elems/separator.rb +17 -0
  47. data/lib/repubmark/elems/special.rb +37 -0
  48. data/lib/repubmark/elems/text.rb +43 -0
  49. data/lib/repubmark/highlight.rb +85 -0
  50. data/lib/repubmark/titled_ref.rb +16 -0
  51. data/lib/repubmark/version.rb +6 -0
  52. data/lib/repubmark.rb +125 -0
  53. data/tests/examples.rb +51 -0
  54. metadata +129 -0
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Joint < Base
6
+ parents :Caption, :Quote
7
+
8
+ def initialize(parent)
9
+ super parent
10
+
11
+ @raw1 = nil
12
+ @base = nil
13
+ @raw2 = nil
14
+ @notes = []
15
+ @raw3 = nil
16
+
17
+ @context = nil
18
+ @context_note = false
19
+ end
20
+
21
+ #################
22
+ # Basic methods #
23
+ #################
24
+
25
+ def word_count = components.sum(&:word_count)
26
+
27
+ def to_summary_plain = components.map(&:to_summary_plain).join.freeze
28
+
29
+ def to_html = components.map(&:to_html).join.freeze
30
+
31
+ def to_gemtext = components.map(&:to_gemtext).join.freeze
32
+
33
+ ########################
34
+ # Builder methods: Raw #
35
+ ########################
36
+
37
+ def raw(str)
38
+ text = Text.new self, str
39
+ if @notes.any?
40
+ raise 'Joint raw text already exists' if @raw3
41
+
42
+ @raw3 = text
43
+ elsif @base
44
+ raise 'Joint raw text already exists' if @raw2
45
+
46
+ @raw2 = text
47
+ else
48
+ raise 'Joint raw text already exists' if @raw1
49
+
50
+ @raw1 = text
51
+ end
52
+ end
53
+
54
+ #########################
55
+ # Builder methods: Base #
56
+ #########################
57
+
58
+ def abbrev(abbrev, transcript) = base Abbrev.new self, abbrev, transcript
59
+
60
+ def bold(str) = base Text.new self, str, bold: true
61
+
62
+ def code_inline(str) = base CodeInline.new self, str
63
+
64
+ def ellipsis = base Special.new self, :ellipsis
65
+
66
+ def fraction(top, bottom) = base Fraction.new self, top, bottom
67
+
68
+ def italic(str) = base Text.new self, str, italic: true
69
+
70
+ def link(text, uri) = base Link.new self, text, uri
71
+
72
+ def link_italic(text, uri) = base Link.new self, text, uri, italic: true
73
+
74
+ def mdash = base Special.new self, :mdash
75
+
76
+ def power(base, exponent) = base Power.new self, base, exponent
77
+
78
+ def quote(str = nil)
79
+ quote = Quote.new self
80
+ case [!!str, block_given?]
81
+ when [true, false] then quote.text str
82
+ when [false, true] then yield quote
83
+ else
84
+ raise 'Invalid args'
85
+ end
86
+ base quote
87
+ end
88
+
89
+ def quote_italic(str)
90
+ quote = Quote.new self
91
+ quote.italic str
92
+ base quote
93
+ end
94
+
95
+ def section(*args) = base Section.new self, *args
96
+
97
+ def text(str) = base Text.new self, str
98
+
99
+ ##########################
100
+ # Builder methods: Notes #
101
+ ##########################
102
+
103
+ def context(index, category, slug)
104
+ raise 'Context already given' if @context
105
+
106
+ @context = [index, category, slug]
107
+ nil
108
+ end
109
+
110
+ def context_note
111
+ raise 'No context given' if @context.nil?
112
+ raise 'Context already noted' if @context_note
113
+
114
+ @context_note = true
115
+ ref_note(*@context)
116
+ end
117
+
118
+ def ref_note(index, category, slug)
119
+ note Note.new self, index, category, slug
120
+ end
121
+
122
+ private
123
+
124
+ def components
125
+ raise 'No context note' if @context && !@context_note
126
+
127
+ [@raw1, @base, @raw2, *@notes, @raw3].compact
128
+ end
129
+
130
+ def base(elem)
131
+ raise 'Joint base already exists' if @base
132
+ raise 'Joint notes already exist' if @notes.any?
133
+
134
+ @base = elem
135
+ nil
136
+ end
137
+
138
+ def note(elem)
139
+ raise 'Joint base does not exists' if @base.nil?
140
+
141
+ @notes << elem
142
+ nil
143
+ end
144
+
145
+ module ForwardingBuilders
146
+ def joint = raise NotImplementedError, "#{self.class}#joint"
147
+
148
+ def abbrev(*args) = joint { |joint| joint.abbrev(*args) }
149
+
150
+ def bold(*args) = joint { |joint| joint.bold(*args) }
151
+
152
+ def code_inline(*args) = joint { |joint| joint.code_inline(*args) }
153
+
154
+ def ellipsis(*args) = joint { |joint| joint.ellipsis(*args) }
155
+
156
+ def fraction(*args) = joint { |joint| joint.fraction(*args) }
157
+
158
+ def italic(*args) = joint { |joint| joint.italic(*args) }
159
+
160
+ def link(*args) = joint { |joint| joint.link(*args) }
161
+
162
+ def link_italic(*args) = joint { |joint| joint.link_italic(*args) }
163
+
164
+ def mdash(*args) = joint { |joint| joint.mdash(*args) }
165
+
166
+ def power(*args) = joint { |joint| joint.power(*args) }
167
+
168
+ def quote_italic(*args) = joint { |joint| joint.quote_italic(*args) }
169
+
170
+ def section(*args) = joint { |joint| joint.section(*args) }
171
+
172
+ def text(*args) = joint { |joint| joint.text(*args) }
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Link < Text
6
+ parents :Joint
7
+
8
+ SCHEMES = %w[http https].freeze
9
+
10
+ def initialize(parent, str, uri, **kwargs)
11
+ super parent, str, **kwargs
12
+
13
+ self.uri = uri
14
+ validate_uri!
15
+ end
16
+
17
+ #################
18
+ # Basic methods #
19
+ #################
20
+
21
+ def to_html
22
+ "<a href=\"#{build_uri('.html')}\">#{str_to_html}</a>".freeze
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :uri
28
+
29
+ def uri=(uri)
30
+ @uri = URI.parse(uri).freeze
31
+ end
32
+
33
+ def validate_uri!
34
+ raise 'Expected normalized URI' unless uri.normalize == uri
35
+ raise 'Expected no userinfo' unless uri.userinfo.nil?
36
+
37
+ validate_uri_http_absolute!
38
+ end
39
+
40
+ def validate_uri_http_absolute!
41
+ return unless uri.is_a?(URI::HTTP) && uri.absolute?
42
+
43
+ raise 'Invalid scheme' unless SCHEMES.include? uri.scheme
44
+ raise 'Expected hostname' unless uri.hostname
45
+ end
46
+
47
+ def build_uri(ext)
48
+ if uri.is_a?(URI::MailTo) || uri.absolute?
49
+ uri
50
+ else
51
+ own_url "#{uri}#{ext}"
52
+ end.to_s.freeze
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class List < Base
6
+ parents :Canvas, :ListItem
7
+
8
+ def initialize(parent, links:, ordered:)
9
+ super parent
10
+
11
+ @links = !!links
12
+ @ordered = !!ordered
13
+
14
+ @items = []
15
+ end
16
+
17
+ #################
18
+ # Basic methods #
19
+ #################
20
+
21
+ def word_count = @items.sum(&:word_count)
22
+
23
+ def to_summary_plain = @items.map(&:to_summary_plain).join(' ').freeze
24
+
25
+ def to_html
26
+ [
27
+ "<#{tag_name}>\n",
28
+ *@items.map(&:to_html),
29
+ "</#{tag_name}>\n",
30
+ ].join.freeze
31
+ end
32
+
33
+ def to_gemtext = @items.map(&:to_gemtext).join.freeze
34
+
35
+ ##################
36
+ # Helper methods #
37
+ ##################
38
+
39
+ def level = parent.instance_of?(ListItem) ? parent.level + 1 : 0
40
+
41
+ def items_count = @items.count
42
+
43
+ def unicode_decor
44
+ return if level <= 1
45
+
46
+ "#{parent.unicode_decor_parent}#{parent.last? ? '⠀ ' : '│ '}"
47
+ end
48
+
49
+ ###################
50
+ # Builder methods #
51
+ ###################
52
+
53
+ def item(...) = @links ? item_links(...) : item_nolinks(...)
54
+
55
+ private
56
+
57
+ def parent=(parent)
58
+ unless parent.instance_of?(Canvas) || parent.instance_of?(ListItem)
59
+ raise TypeError,
60
+ "Expected #{Canvas} or #{ListItem}, got #{parent.class}"
61
+ end
62
+
63
+ @parent = parent
64
+ end
65
+
66
+ def tag_name = @ordered ? 'ol' : 'ul'
67
+
68
+ def item_nolinks
69
+ list_item = ListItem.new self, @items.count
70
+ @items << list_item
71
+ yield list_item
72
+ nil
73
+ end
74
+
75
+ def item_links(ref_url, ref_title = nil)
76
+ titled_ref = TitledRef.new ref_url, ref_title
77
+ list_item = ListItem.new self, @items.count, titled_ref
78
+ @items << list_item
79
+ yield list_item if block_given?
80
+ nil
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class ListItem < Base
6
+ extend Forwardable
7
+
8
+ parents :List
9
+
10
+ attr_reader :index, :titled_ref
11
+
12
+ def initialize(parent, index, titled_ref = nil)
13
+ super parent
14
+
15
+ self.index = index
16
+ self.titled_ref = titled_ref
17
+
18
+ @caption = Caption.new self
19
+ @sublist = nil
20
+ end
21
+
22
+ #################
23
+ # Basic methods #
24
+ #################
25
+
26
+ def word_count = @caption.word_count + (@sublist&.word_count || 0)
27
+
28
+ def to_summary_plain
29
+ [
30
+ @caption.to_summary_plain,
31
+ @sublist&.to_summary_plain,
32
+ ].compact.join("\n").freeze
33
+ end
34
+
35
+ def to_html
36
+ [
37
+ "<li>\n",
38
+ build_ref(:html),
39
+ @caption.to_html,
40
+ @sublist&.to_html,
41
+ "</li>\n",
42
+ ].join.freeze
43
+ end
44
+
45
+ def to_gemtext
46
+ [
47
+ if titled_ref
48
+ "#{build_ref(:gemtext)}#{@caption.to_gemtext}".strip
49
+ else
50
+ "* #{unicode_decor_own}#{@caption.to_gemtext}".strip
51
+ end,
52
+ @sublist&.to_gemtext,
53
+ ].join("\n").freeze
54
+ end
55
+
56
+ ##################
57
+ # Helper methods #
58
+ ##################
59
+
60
+ def level = parent.level
61
+
62
+ def last? = index >= parent.items_count - 1
63
+
64
+ def unicode_decor_own
65
+ "#{parent.unicode_decor}#{last? ? '└' : '├'} " unless level.zero?
66
+ end
67
+
68
+ def unicode_decor_parent = parent.unicode_decor
69
+
70
+ ###################
71
+ # Builder methods #
72
+ ###################
73
+
74
+ def subolist
75
+ raise 'No nested link lists' if titled_ref
76
+ raise 'Sublist already exists' if @sublist
77
+
78
+ @sublist = List.new self, links: false, ordered: true
79
+ yield @sublist
80
+ nil
81
+ end
82
+
83
+ def subulist
84
+ raise 'No nested link lists' if titled_ref
85
+ raise 'Sublist already exists' if @sublist
86
+
87
+ @sublist = List.new self, links: false, ordered: false
88
+ yield @sublist
89
+ nil
90
+ end
91
+
92
+ def respond_to_missing?(method_name, _include_private)
93
+ @caption.respond_to?(method_name) || super
94
+ end
95
+
96
+ def method_missing(method_name, ...)
97
+ if @caption.respond_to? method_name
98
+ raise 'Caption after sublist' if @sublist
99
+
100
+ @caption.public_send(method_name, ...)
101
+ else
102
+ super
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def parent=(parent)
109
+ unless parent.instance_of? List
110
+ raise TypeError, "Expected #{List}, got #{parent.class}"
111
+ end
112
+
113
+ @parent = parent
114
+ end
115
+
116
+ def index=(index)
117
+ index = Integer index
118
+ raise 'Invalid index' if index.negative?
119
+
120
+ @index = index
121
+ end
122
+
123
+ def titled_ref=(titled_ref)
124
+ return @titled_ref = nil if titled_ref.nil?
125
+
126
+ unless titled_ref.instance_of? TitledRef
127
+ raise TypeError, "Expected #{TitledRef}, got #{titled_ref.class}"
128
+ end
129
+
130
+ @titled_ref = titled_ref
131
+ end
132
+
133
+ def build_ref(format)
134
+ return if titled_ref.nil?
135
+
136
+ case format
137
+ when :html
138
+ "<a href=\"#{titled_ref.url}\">#{titled_ref.title}</a>\n".freeze
139
+ when :gemtext
140
+ "=> #{titled_ref.url} #{unicode_decor_own}#{titled_ref.title} ".freeze
141
+ else
142
+ raise 'Invalid format'
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Note < Base
6
+ parents :Joint
7
+
8
+ def initialize(parent, index, category, slug)
9
+ super parent
10
+ @index = index
11
+ @category = category
12
+ @slug = slug
13
+ end
14
+
15
+ #################
16
+ # Basic methods #
17
+ #################
18
+
19
+ def to_html
20
+ [
21
+ '<sup>',
22
+ %(<a href="##{CGI.escape_html(anchor)}">),
23
+ %([#{CGI.escape_html(@index.to_s)}]),
24
+ '</a>',
25
+ '</sup>',
26
+ ].join.freeze
27
+ end
28
+
29
+ def to_gemtext = "⁽#{index_unicode_sup}⁾".freeze
30
+
31
+ private
32
+
33
+ def anchor = "#@category-#@slug"
34
+
35
+ def index_unicode_sup
36
+ @index_unicode_sup ||= Repubmark.unicode_sup @index
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Paragraph < Base
6
+ parents :Canvas
7
+
8
+ def initialize(parent)
9
+ super parent
10
+ @caption = Caption.new self
11
+ end
12
+
13
+ #################
14
+ # Basic methods #
15
+ #################
16
+
17
+ def word_count = @caption.word_count
18
+
19
+ def to_summary_plain = @caption.to_summary_plain
20
+
21
+ def to_html = "<p>\n#{@caption.to_html}</p>\n".freeze
22
+
23
+ def to_gemtext = "#{@caption.to_gemtext}\n".freeze
24
+
25
+ ###################
26
+ # Builder methods #
27
+ ###################
28
+
29
+ def respond_to_missing?(method_name, _include_private)
30
+ @caption.respond_to?(method_name) || super
31
+ end
32
+
33
+ def method_missing(method_name, ...)
34
+ if @caption.respond_to?(method_name)
35
+ @caption.public_send(method_name, ...)
36
+ else
37
+ super
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Power < Base
6
+ parents :Joint
7
+
8
+ def initialize(parent, base, exponent)
9
+ super parent
10
+
11
+ @base = Integer base
12
+ @exponent = Integer exponent
13
+ end
14
+
15
+ #################
16
+ # Basic methods #
17
+ #################
18
+
19
+ def word_count = 1
20
+
21
+ def to_summary_plain = "#@base#{unicode_exponent}".freeze
22
+
23
+ def to_html = "#@base<sup>#@exponent</sup>".freeze
24
+
25
+ def to_gemtext = "#@base#{unicode_exponent}".freeze
26
+
27
+ private
28
+
29
+ def unicode_exponent
30
+ @unicode_exponent ||= Repubmark.unicode_sup @exponent
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Quote < Base
6
+ include Joint::ForwardingBuilders
7
+
8
+ parents :Caption, :Joint, :Quote
9
+
10
+ def initialize(...)
11
+ super
12
+ @items = []
13
+ end
14
+
15
+ #################
16
+ # Basic methods #
17
+ #################
18
+
19
+ def word_count = @items.sum(&:word_count)
20
+
21
+ def to_summary_plain
22
+ "«#{@items.map(&:to_summary_plain).join(' ')}»".freeze
23
+ end
24
+
25
+ def to_html = "&laquo;#{@items.map(&:to_html).join("\n")}&raquo;".freeze
26
+
27
+ def to_gemtext = "«#{@items.map(&:to_gemtext).join(' ')}»".freeze
28
+
29
+ ###################
30
+ # Builder methods #
31
+ ###################
32
+
33
+ def joint
34
+ joint = Joint.new self
35
+ yield joint
36
+ @items << joint
37
+ nil
38
+ end
39
+
40
+ def quote(str = nil)
41
+ quote = Quote.new self
42
+ case [!!str, block_given?]
43
+ when [true, false] then quote.text str
44
+ when [false, true] then yield quote
45
+ else
46
+ raise 'Invalid args'
47
+ end
48
+ @items << quote
49
+ nil
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Section < Base
6
+ parents :Joint
7
+
8
+ SECT_HTML = '&sect;'
9
+ SECT_UNICODE = '§'
10
+
11
+ attr_reader :count, :text
12
+
13
+ def initialize(parent, *args)
14
+ super parent
15
+
16
+ case args.length
17
+ when 1
18
+ self.count = 1
19
+ self.text = args[0]
20
+ when 2
21
+ self.count, self.text = args
22
+ else
23
+ raise ArgumentError, 'Expected 1 or 2 arguments'
24
+ end
25
+ end
26
+
27
+ #################
28
+ # Basic methods #
29
+ #################
30
+
31
+ def word_count = count_words @text
32
+
33
+ def to_summary_plain = "#{SECT_UNICODE * count}#{text}".freeze
34
+
35
+ def to_html = "#{SECT_HTML * count}#{text}".freeze
36
+
37
+ def to_gemtext = "#{SECT_UNICODE * count}#{text}".freeze
38
+
39
+ private
40
+
41
+ def count=(count)
42
+ unless count.instance_of? Integer
43
+ raise TypeError, "Expected #{Integer}, got #{count.class}"
44
+ end
45
+ raise 'Expected positive count' unless count.positive?
46
+
47
+ @count = count
48
+ end
49
+
50
+ def text=(text)
51
+ text = String(text).strip.freeze
52
+ raise 'Expected non-empty text' if text.empty?
53
+
54
+ @text = text
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Separator < Base
6
+ parents :Canvas
7
+
8
+ #################
9
+ # Basic methods #
10
+ #################
11
+
12
+ def to_html = "<hr/>\n"
13
+
14
+ def to_gemtext = "---\n"
15
+ end
16
+ end
17
+ end