repubmark 0.0.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.
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,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Chapter < Base
6
+ parents :Article, :Chapter
7
+
8
+ def initialize(parent, level = 1, slug = nil, title = nil)
9
+ super parent
10
+ self.level = level
11
+ self.slug = slug
12
+ self.title = title
13
+ verify!
14
+
15
+ @canvas = Canvas.new self
16
+ @chapters = []
17
+ end
18
+
19
+ #################
20
+ # Basic methods #
21
+ #################
22
+
23
+ def word_count
24
+ (@title ? count_words(@title) : 0) +
25
+ @canvas.word_count +
26
+ @chapters.sum(&:word_count)
27
+ end
28
+
29
+ def to_summary_plain
30
+ str = [
31
+ @title,
32
+ @canvas.to_summary_plain,
33
+ *@chapters.map(&:to_summary_plain),
34
+ ].compact.join(' ').strip.freeze
35
+
36
+ str.empty? ? nil : str
37
+ end
38
+
39
+ def to_html
40
+ [
41
+ build_title_html,
42
+ @canvas.to_html,
43
+ *@chapters.map(&:to_html),
44
+ ].join.freeze
45
+ end
46
+
47
+ def to_gemtext
48
+ [
49
+ build_title_gemtext,
50
+ @canvas.to_gemtext,
51
+ *@chapters.map(&:to_gemtext),
52
+ ].join.freeze
53
+ end
54
+
55
+ def chapters
56
+ {
57
+ slug: anchor,
58
+ title: @title,
59
+ chapters: @chapters.map(&:chapters).freeze,
60
+ }.freeze
61
+ end
62
+
63
+ ##################
64
+ # Helper methods #
65
+ ##################
66
+
67
+ def anchor
68
+ if parent.instance_of?(self.class) && @level > 2
69
+ "#{parent.anchor}--#@slug"
70
+ elsif anchor_prefix.empty?
71
+ @slug
72
+ else
73
+ "#{anchor_prefix}--#@slug"
74
+ end.freeze
75
+ end
76
+
77
+ ###################
78
+ # Builder methods #
79
+ ###################
80
+
81
+ def chapter(slug, title)
82
+ chapter = Chapter.new self, @level + 1, slug, title
83
+ @chapters << chapter
84
+ yield chapter
85
+ nil
86
+ end
87
+
88
+ def respond_to_missing?(method_name, _include_private)
89
+ @canvas.respond_to?(method_name) || super
90
+ end
91
+
92
+ def method_missing(method_name, ...)
93
+ if @canvas.respond_to? method_name
94
+ raise 'Intro after chapters' unless @chapters.empty?
95
+
96
+ @canvas.public_send(method_name, ...)
97
+ else
98
+ super
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def level=(level)
105
+ level = Integer level
106
+ raise unless level.positive?
107
+
108
+ @level = level
109
+ end
110
+
111
+ def slug=(slug)
112
+ @slug = Repubmark.validate_slug! slug if slug
113
+ end
114
+
115
+ def title=(title)
116
+ return @title = nil if title.nil?
117
+
118
+ title = String(title).split.join(' ').strip.freeze
119
+ raise 'Empty title' if title.empty?
120
+
121
+ @title = title
122
+ end
123
+
124
+ def verify!
125
+ if @level == 1
126
+ raise 'Non-empty slug for level 1' if @slug
127
+ raise 'Non-empty title for level 1' if @title
128
+ else
129
+ raise 'Empty slug for level >= 2' if @slug.nil?
130
+ raise 'Empty title for level >= 2' if @title.nil?
131
+ end
132
+ end
133
+
134
+ def build_title_html
135
+ id_attr = %( id="#{anchor}") unless anchor_prefix.empty?
136
+ %(<h#@level#{id_attr}>#@title</h#@level>\n) if @level != 1
137
+ end
138
+
139
+ def build_title_gemtext
140
+ "\n#{'#' * @level} #@title\n\n" if @level != 1
141
+ end
142
+
143
+ def anchor_prefix
144
+ @anchor_prefix ||= String(config[:chapter_anchor_prefix]).strip.freeze
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class CodeBlock < Base
6
+ parents :Canvas
7
+
8
+ attr_reader :syntax, :str, :indent
9
+
10
+ def initialize(parent, syntax, str, indent: 0)
11
+ super parent
12
+
13
+ self.syntax = syntax
14
+ self.str = str
15
+ self.indent = indent
16
+ end
17
+
18
+ #################
19
+ # Basic methods #
20
+ #################
21
+
22
+ def to_html = highlighted_str
23
+
24
+ def to_gemtext
25
+ [
26
+ "```\n",
27
+ "#{str.strip}\n",
28
+ "```\n",
29
+ ].join.freeze
30
+ end
31
+
32
+ private
33
+
34
+ def syntax=(syntax)
35
+ return @syntax = nil if syntax.nil?
36
+
37
+ unless syntax.instance_of? Symbol
38
+ raise TypeError, "Expected #{Symbol}, got #{syntax.class}"
39
+ end
40
+
41
+ @syntax = syntax
42
+ end
43
+
44
+ def str=(str)
45
+ str = String(str).freeze
46
+ raise 'Expected non-blank string' if str.strip.empty?
47
+
48
+ @str = str
49
+ end
50
+
51
+ def indent=(indent)
52
+ @indent = Integer indent
53
+ raise 'Expected non-negative number' if @indent.negative?
54
+ end
55
+
56
+ def highlighted_str
57
+ @highlighted_str ||= Highlight.call syntax, transformed_str
58
+ end
59
+
60
+ def transformed_str
61
+ @transformed_str ||=
62
+ str.lines.map { |line| "#{indentation}#{line}" }.join.freeze
63
+ end
64
+
65
+ def indentation
66
+ @indentation ||= (' ' * indent).freeze
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class CodeInline < Base
6
+ parents :Joint
7
+
8
+ def initialize(parent, str)
9
+ super parent
10
+ self.str = str
11
+ end
12
+
13
+ #################
14
+ # Basic methods #
15
+ #################
16
+
17
+ def to_summary_plain = "«#{@str}»".freeze
18
+
19
+ def to_html = "<code>#{CGI.escape_html(@str)}</code>".freeze
20
+
21
+ def to_gemtext = "«#{@str}»".freeze
22
+
23
+ private
24
+
25
+ def str=(str)
26
+ str = String(str).freeze
27
+ if str.include?("\n") || str.include?('«') || str.include?('»')
28
+ raise 'Invalid str'
29
+ end
30
+
31
+ @str = str
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Figure < Base
6
+ parents :Canvas, :Figures
7
+
8
+ def initialize(parent, name, alt)
9
+ super parent
10
+
11
+ alt = String(alt).strip.split.join(' ').freeze
12
+ raise 'Empty alt text' if alt.empty?
13
+
14
+ @name = String(name).strip.freeze
15
+ @alt = alt
16
+ @caption = Caption.new self
17
+ end
18
+
19
+ #################
20
+ # Basic methods #
21
+ #################
22
+
23
+ def to_html
24
+ [
25
+ "<div#{html_class(:figure_wrap)}>\n",
26
+ "<figure#{html_class(:figure_self)}>\n",
27
+ "<img src=\"#{src}\" alt=\"#{CGI.escape_html(@alt)}\"/>\n",
28
+ "<figcaption>\n",
29
+ caption_html,
30
+ "</figcaption>\n",
31
+ "</figure>\n",
32
+ "</div>\n",
33
+ ].join.freeze
34
+ end
35
+
36
+ def to_gemtext
37
+ caption = @caption.to_gemtext.to_s.strip
38
+ caption = @alt if caption.empty?
39
+ "=> #{src} #{caption}\n".freeze
40
+ end
41
+
42
+ ###################
43
+ # Builder methods #
44
+ ###################
45
+
46
+ def respond_to_missing?(method_name, _include_private)
47
+ @caption.respond_to?(method_name) || super
48
+ end
49
+
50
+ def method_missing(method_name, ...)
51
+ if @caption.respond_to? method_name
52
+ @caption.public_send(method_name, ...)
53
+ else
54
+ super
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def src
61
+ @src ||= own_url "#{config[:images_prefix]}/#@name"
62
+ end
63
+
64
+ def caption_html
65
+ if @caption.empty?
66
+ "#{CGI.escape_html(@alt)}\n"
67
+ else
68
+ @caption.to_html
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Figures < Base
6
+ parents :Canvas
7
+
8
+ def initialize(parent)
9
+ super parent
10
+ @figures = []
11
+ end
12
+
13
+ #################
14
+ # Basic methods #
15
+ #################
16
+
17
+ def to_html
18
+ raise 'Expected two figures' unless @figures.size == 2
19
+
20
+ [
21
+ "<div#{html_class(:figures_wrap)}>\n",
22
+ *@figures.flat_map do |figure|
23
+ [
24
+ "<div#{html_class(:figures_left)}>\n",
25
+ figure.to_html,
26
+ "</div>\n",
27
+ ]
28
+ end,
29
+ "</div>\n",
30
+ ].join.freeze
31
+ end
32
+
33
+ def to_gemtext = @figures.map(&:to_gemtext).join.freeze
34
+
35
+ ###################
36
+ # Builder methods #
37
+ ###################
38
+
39
+ def figure(name, alt)
40
+ figure = Figure.new self, name, alt
41
+ @figures << figure
42
+ yield figure if block_given?
43
+ nil
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Footnote < Base
6
+ parents :FootnotesCategory
7
+
8
+ attr_reader :category, :slug, :index, :url, :link_text, :date, :alt_urls
9
+
10
+ def initialize(parent, **kwargs)
11
+ super parent
12
+ kwargs.each { |k, v| send :"#{k}=", v }
13
+ @caption = Caption.new self
14
+ end
15
+
16
+ #################
17
+ # Basic methods #
18
+ #################
19
+
20
+ def to_html
21
+ result = ''
22
+ result +=
23
+ %(<li id="#{category}-#{slug}" value="#{index}" class="fragment-highlight">\n)
24
+ if date
25
+ result += %(<span class="text-muted">\n)
26
+ result += %(<time datetime="#{date}">\n)
27
+ result += "#{I18n.localize(Date.parse(date), format: :long)}\n"
28
+ result += "</time>\n"
29
+ result += "</span>\n"
30
+ end
31
+ if url
32
+ scheme = Addressable::URI.parse(url).scheme&.downcase
33
+ if scheme
34
+ unless %w[http https].include? scheme
35
+ result += %(<span class="badge text-bg-secondary">\n)
36
+ result += %(#{scheme}://\n)
37
+ result += "</span>\n"
38
+ end
39
+ end
40
+ result += %(<a href="#{url}">#{link_text}</a>\n)
41
+ else
42
+ result += "#{link_text}\n" if link_text
43
+ end
44
+ result += @caption.to_html.to_s
45
+ if alt_urls&.any?
46
+ result += %[(#{
47
+ alt_urls.map do |alt_url|
48
+ %(<a href=\"#{alt_url[:url]}\">#{alt_url[:name]}</a>)
49
+ end.join ', '
50
+ })\n]
51
+ end
52
+ result += '</li>'
53
+ result.freeze
54
+ end
55
+
56
+ def to_gemtext
57
+ result = ''
58
+ result += url ? "=> #{url}" : '*'
59
+ result += " #{index})"
60
+ result += " #{Date.parse(date)}" if date
61
+ result += " #{link_text}" if link_text
62
+ result += " #{@caption.to_gemtext}"
63
+ result.freeze
64
+ end
65
+
66
+ ###################
67
+ # Builder methods #
68
+ ###################
69
+
70
+ def respond_to_missing?(method_name, _include_private)
71
+ @caption.respond_to?(method_name) || super
72
+ end
73
+
74
+ def method_missing(method_name, ...)
75
+ if @caption.respond_to? method_name
76
+ @caption.public_send(method_name, ...)
77
+ else
78
+ super
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ attr_writer :category, :slug, :index, :url, :link_text, :date, :alt_urls
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class FootnotesCategory < Base
6
+ parents :Article
7
+
8
+ def initialize(parent, title)
9
+ super parent
10
+ @title = title
11
+ @footnotes = []
12
+ end
13
+
14
+ #################
15
+ # Basic methods #
16
+ #################
17
+
18
+ def to_html
19
+ <<~HTML
20
+ <h2>#@title</h2>
21
+
22
+ <ol>
23
+ #{@footnotes.map(&:to_html).map(&:strip).join("\n")}
24
+ </ol>
25
+ HTML
26
+ end
27
+
28
+ def to_gemtext
29
+ <<~GEMTEXT
30
+ ## #@title
31
+
32
+ #{@footnotes.map(&:to_gemtext).map(&:strip).join("\n")}
33
+ GEMTEXT
34
+ end
35
+
36
+ ###################
37
+ # Builder methods #
38
+ ###################
39
+
40
+ def footnote(**kwargs)
41
+ footnote = Footnote.new(self, **kwargs)
42
+ @footnotes << footnote
43
+ yield footnote if block_given?
44
+ nil
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Fraction < Base
6
+ parents :Joint
7
+
8
+ def initialize(parent, top, bottom)
9
+ super parent
10
+
11
+ @top = Integer top
12
+ @bottom = Integer bottom
13
+
14
+ raise 'Expected top to be non-negative' if @top.negative?
15
+ raise 'Expected bottom to be positive' unless @bottom.positive?
16
+ end
17
+
18
+ #################
19
+ # Basic methods #
20
+ #################
21
+
22
+ def word_count = 1
23
+
24
+ def to_summary_plain = "#@top/#@bottom".freeze
25
+
26
+ def to_html = "<sup>#@top</sup>&frasl;<sub>#@bottom</sub>".freeze
27
+
28
+ def to_gemtext = "#@top/#@bottom".freeze
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Iframe < Base
6
+ parents :Canvas
7
+
8
+ attr_reader :title, :src, :url
9
+
10
+ def initialize(parent, title, src, url = nil)
11
+ super parent
12
+
13
+ self.title = title
14
+ self.src = src
15
+ self.url = url || src
16
+ end
17
+
18
+ #################
19
+ # Basic methods #
20
+ #################
21
+
22
+ def to_html
23
+ [
24
+ "<div#{html_class(:iframe_wrap)}>\n",
25
+ "<iframe\n",
26
+ %(title="#{title}"\n),
27
+ %(src="#{src}"\n),
28
+ %(allowfullscreen=""\n),
29
+ %(frameborder="0"\n),
30
+ %(sandbox="allow-same-origin allow-scripts allow-popups"\n),
31
+ "></iframe>\n",
32
+ "</div>\n",
33
+ ].join.freeze
34
+ end
35
+
36
+ def to_gemtext = "=> #{url} #{title}\n"
37
+
38
+ private
39
+
40
+ def title=(title)
41
+ title = String(title).strip.freeze
42
+ raise 'Empty title' if title.empty?
43
+
44
+ @title = title
45
+ end
46
+
47
+ def src=(src)
48
+ src = String(src).strip.freeze
49
+ raise 'Empty src' if src.empty?
50
+
51
+ @src = src
52
+ end
53
+
54
+ def url=(url)
55
+ url = String(url).strip.freeze
56
+ raise 'Empty url' if url.empty?
57
+
58
+ @url = url
59
+ end
60
+ end
61
+ end
62
+ end