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