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
data/exe/repubmark ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ lib = File.expand_path('../lib', __dir__).freeze
5
+ $LOAD_PATH.unshift lib unless $LOAD_PATH.include? lib
6
+
7
+ require 'bundler/setup'
8
+
9
+ require 'i18n'
10
+ require 'repubmark'
11
+
12
+ I18n.load_path = Dir[
13
+ Pathname.new(__dir__).join('../vendor/rails-i18n/rails/locale/**/*.yml')
14
+ ].each(&:freeze).freeze
15
+
16
+ config_filename = String(ARGV[0]).freeze
17
+ profile = String(ARGV[1]).freeze
18
+
19
+ config = Repubmark::Config.load_file config_filename, profile
20
+ $article = Repubmark::Elems::Article.new config
21
+
22
+ template = $stdin.read.freeze
23
+ context = BasicObject.new
24
+ def context.article = $article
25
+ context.instance_eval template, __FILE__, __LINE__
26
+
27
+ def wrap(str)
28
+ words = str.strip.split
29
+ lines = [[]]
30
+ words.each do |word|
31
+ lines << [] if [*lines.last, word].join(' ').length > 70
32
+ lines.last << word
33
+ end
34
+ lines.map { |line| "#{line.join(' ')}\n" }.join.freeze
35
+ end
36
+
37
+ output =
38
+ case config.format
39
+ when :chapters then JSON.pretty_generate $article.chapters
40
+ when :gemtext then $article.to_gemtext
41
+ when :html then $article.to_html
42
+ when :summary_plain then wrap $article.to_summary_plain
43
+ when :word_count then $article.word_count
44
+ end.to_s.strip
45
+
46
+ puts output unless output.empty?
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ class Config
5
+ OPTIONAL_KEYS = %i[
6
+ base_url
7
+ chapter_anchor_prefix
8
+ css_class_annotation
9
+ css_class_blockquote_figure
10
+ css_class_blockquote_blockquote
11
+ css_class_blockquote_figcaption
12
+ css_class_figure_self
13
+ css_class_figure_wrap
14
+ css_class_figures_left
15
+ css_class_figures_right
16
+ css_class_figures_wrap
17
+ css_class_iframe_wrap
18
+ current_path
19
+ images_prefix
20
+ relative_urls
21
+ ].freeze
22
+
23
+ attr_reader :format
24
+
25
+ def self.load_file(filename, profile)
26
+ config_data = YAML.safe_load_file filename, aliases: true
27
+ profile_data = config_data[profile]
28
+ raise 'Unknown profile' if profile_data.nil?
29
+
30
+ new(**profile_data.transform_keys(&:to_sym))
31
+ end
32
+
33
+ def initialize(format:, **kwargs)
34
+ self.format = format
35
+ raise unless (kwargs.keys - OPTIONAL_KEYS).empty?
36
+
37
+ @kwargs = kwargs.freeze
38
+ end
39
+
40
+ def [](key)
41
+ OPTIONAL_KEYS.include?(key) ? @kwargs[key] : @kwargs.fetch(key)
42
+ end
43
+
44
+ private
45
+
46
+ def format=(format)
47
+ format = format.to_sym if format.instance_of? String
48
+ raise 'Invalid format' unless FORMATS.include? format
49
+
50
+ @format = format
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Abbrev < Base
6
+ parents :Joint
7
+
8
+ attr_reader :abbrev, :transcript
9
+
10
+ def initialize(parent, abbrev, transcript)
11
+ super parent
12
+
13
+ self.abbrev = abbrev
14
+ self.transcript = transcript
15
+ end
16
+
17
+ #################
18
+ # Basic methods #
19
+ #################
20
+
21
+ def word_count = 1
22
+
23
+ def to_summary_plain = abbrev
24
+
25
+ def to_html
26
+ %(<abbr title="#{escape_transcript}">#{escape_abbrev}</abbr>).freeze
27
+ end
28
+
29
+ def to_gemtext = abbrev
30
+
31
+ private
32
+
33
+ def abbrev=(abbrev)
34
+ @abbrev =
35
+ String(abbrev).split.join(' ').freeze.tap do |new_abbrev|
36
+ raise 'Empty string' if new_abbrev.empty?
37
+ end
38
+ end
39
+
40
+ def transcript=(transcript)
41
+ @transcript =
42
+ String(transcript).split.join(' ').freeze.tap do |new_transcript|
43
+ raise 'Empty string' if new_transcript.empty?
44
+ end
45
+ end
46
+
47
+ def escape_abbrev = CGI.escape_html(abbrev).freeze
48
+
49
+ def escape_transcript = CGI.escape_html(transcript).freeze
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Annotation < Base
6
+ parents :Article
7
+
8
+ def initialize(parent)
9
+ super parent
10
+ @canvas = Canvas.new self
11
+ end
12
+
13
+ #################
14
+ # Basic methods #
15
+ #################
16
+
17
+ def word_count = @canvas.word_count
18
+
19
+ def to_summary_plain = @canvas.to_summary_plain
20
+
21
+ def to_html
22
+ [
23
+ %(<div#{html_class(:annotation)}>\n),
24
+ @canvas.to_html,
25
+ "</div>\n",
26
+ ].join.freeze
27
+ end
28
+
29
+ def to_gemtext = @canvas.to_gemtext
30
+
31
+ ###################
32
+ # Builder methods #
33
+ ###################
34
+
35
+ def respond_to_missing?(method_name, _include_private)
36
+ @canvas.respond_to?(method_name) || super
37
+ end
38
+
39
+ def method_missing(method_name, ...)
40
+ if @canvas.respond_to? method_name
41
+ @canvas.public_send(method_name, ...)
42
+ else
43
+ super
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Article < Base
6
+ attr_reader :config
7
+
8
+ def initialize(config) # rubocop:disable Lint/MissingSuper
9
+ @parent = nil
10
+ self.config = config
11
+ @footnotes_categories = []
12
+ end
13
+
14
+ #################
15
+ # Basic methods #
16
+ #################
17
+
18
+ def word_count
19
+ (@annotation&.word_count || 0) +
20
+ (@chapter&.word_count || 0)
21
+ end
22
+
23
+ def to_summary_plain
24
+ [
25
+ @annotation&.to_summary_plain,
26
+ @chapter&.to_summary_plain,
27
+ ].compact.join(' ').freeze
28
+ end
29
+
30
+ def to_html
31
+ [
32
+ @annotation&.to_html,
33
+ @chapter&.to_html,
34
+ @footnotes_categories.any? ? "<div>\n<hr/>\n" : nil,
35
+ *@footnotes_categories.map(&:to_html),
36
+ @footnotes_categories.any? ? "</div>\n" : nil,
37
+ ].compact.join.freeze
38
+ end
39
+
40
+ def to_gemtext
41
+ [
42
+ @annotation&.to_gemtext,
43
+ @chapter&.to_gemtext,
44
+ *@footnotes_categories.map(&:to_gemtext),
45
+ ].compact.join("\n\n\n").freeze
46
+ end
47
+
48
+ def chapters = @chapter&.chapters[:chapters] || [].freeze
49
+
50
+ ###################
51
+ # Builder methods #
52
+ ###################
53
+
54
+ def annotation
55
+ raise 'Annotation already exists' if @annotation
56
+ raise 'Annotation after chapters' if @chapter
57
+
58
+ @annotation = Annotation.new self
59
+ yield @annotation
60
+ nil
61
+ end
62
+
63
+ def respond_to_missing?(method_name, _include_private)
64
+ chapter = @chapter || Chapter.new(self)
65
+ chapter.respond_to?(method_name) || super
66
+ end
67
+
68
+ def method_missing(method_name, ...)
69
+ chapter = @chapter || Chapter.new(self)
70
+ if chapter.respond_to? method_name
71
+ @chapter = chapter
72
+ @chapter.public_send(method_name, ...)
73
+ else
74
+ super
75
+ end
76
+ end
77
+
78
+ def footnotes_category(name)
79
+ footnotes_category = FootnotesCategory.new self, name
80
+ @footnotes_categories << footnotes_category
81
+ yield footnotes_category
82
+ nil
83
+ end
84
+
85
+ private
86
+
87
+ def config=(config)
88
+ unless config.instance_of? Config
89
+ raise TypeError, "Expected #{Config}, got #{config.class}"
90
+ end
91
+
92
+ @config = config
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Base
6
+ attr_reader :parent
7
+
8
+ def initialize(parent)
9
+ self.parent = parent
10
+ end
11
+
12
+ #################
13
+ # Basic methods #
14
+ #################
15
+
16
+ def word_count = 0
17
+
18
+ def to_summary_plain = nil
19
+
20
+ def to_html
21
+ raise NotImplementedError, "#{self.class}#to_html"
22
+ end
23
+
24
+ def to_gemtext
25
+ raise NotImplementedError, "#{self.class}#to_gemtext"
26
+ end
27
+
28
+ ##################
29
+ # Helper methods #
30
+ ##################
31
+
32
+ def config = parent.config
33
+
34
+ class << self
35
+ def parents(*args)
36
+ if @parents
37
+ raise ArgumentError, 'Invalid args' unless args.empty?
38
+
39
+ return @parents
40
+ end
41
+
42
+ @parents = args.map { |arg| "#{Elems}::#{arg}".freeze }.to_a.freeze
43
+ nil
44
+ end
45
+
46
+ def parent?(klass)
47
+ unless klass.instance_of? Class
48
+ raise TypeError, "Expected #{Class}, got #{klass.class}"
49
+ end
50
+
51
+ parents.include? klass.name
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def parent=(parent)
58
+ unless parent.is_a? Base
59
+ raise TypeError, "Expected #{Base}, got #{parent.class}"
60
+ end
61
+
62
+ unless self.class.parent? parent.class
63
+ raise TypeError,
64
+ "Expected #{self.class.parents.join(', ')}, got #{parent.class}"
65
+ end
66
+
67
+ @parent = parent
68
+ end
69
+
70
+ def own_url(path)
71
+ config[:relative_urls] ? relative_url(path) : absolute_url(path)
72
+ end
73
+
74
+ def absolute_url(path)
75
+ base_url = String(config[:base_url]).strip.freeze
76
+ raise 'Invalid base URL' if base_url.empty?
77
+
78
+ path = "/#{path}" unless path.start_with? '/'
79
+ "#{base_url}#{path}"
80
+ end
81
+
82
+ def relative_url(path)
83
+ current_path = String(config[:current_path]).strip.freeze
84
+ raise 'Invalid current path URL' if current_path.empty?
85
+
86
+ Pathname
87
+ .new(path)
88
+ .relative_path_from("/#{current_path}/..")
89
+ .to_s
90
+ .freeze
91
+ end
92
+
93
+ def html_class(key)
94
+ if (value = config[:"css_class_#{key}"])
95
+ %( class="#{value}").freeze
96
+ else
97
+ ''
98
+ end
99
+ end
100
+
101
+ def count_words(str) = str.split.count
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Blockquote < Base
6
+ parents :Canvas
7
+
8
+ def initialize(parent)
9
+ super parent
10
+
11
+ @canvas = Canvas.new self
12
+ @caption = nil
13
+ end
14
+
15
+ #################
16
+ # Basic methods #
17
+ #################
18
+
19
+ def word_count = @canvas.word_count + (@caption&.word_count || 0)
20
+
21
+ def to_html
22
+ [
23
+ "<figure#{html_class(:blockquote_figure)}>\n",
24
+ "<blockquote#{html_class(:blockquote_blockquote)}>\n",
25
+ @canvas.to_html,
26
+ "</blockquote>\n",
27
+ caption_html,
28
+ "</figure>\n",
29
+ ].join.freeze
30
+ end
31
+
32
+ def to_gemtext
33
+ [
34
+ @canvas.to_gemtext.split("\n").map { |s| "> #{s}\n" }.join,
35
+ caption_gemtext,
36
+ ].join.freeze
37
+ end
38
+
39
+ ###################
40
+ # Builder methods #
41
+ ###################
42
+
43
+ def caption
44
+ @caption = Caption.new self
45
+ yield @caption
46
+ nil
47
+ end
48
+
49
+ def respond_to_missing?(method_name, _include_private)
50
+ @canvas.respond_to?(method_name) || super
51
+ end
52
+
53
+ def method_missing(method_name, ...)
54
+ if @canvas.respond_to? method_name
55
+ raise 'Paragraph after caption' unless @caption.nil?
56
+
57
+ @canvas.public_send(method_name, ...)
58
+ else
59
+ super
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def caption_html
66
+ return if @caption.nil?
67
+
68
+ [
69
+ "<figcaption#{html_class(:blockquote_figcaption)}>\n",
70
+ @caption.to_html,
71
+ "</figcaption>\n",
72
+ ].join.freeze
73
+ end
74
+
75
+ def caption_gemtext
76
+ "#{@caption.to_gemtext}\n".freeze unless @caption.nil?
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Canvas < Base
6
+ parents :Annotation, :Blockquote, :Chapter
7
+
8
+ def initialize(parent)
9
+ super parent
10
+ @items = []
11
+ end
12
+
13
+ #################
14
+ # Basic methods #
15
+ #################
16
+
17
+ def word_count = @items.sum(&:word_count)
18
+
19
+ def to_summary_plain
20
+ return if @items.empty?
21
+
22
+ @items.filter_map(&:to_summary_plain).join(' ').freeze
23
+ end
24
+
25
+ def to_html
26
+ @items.map(&:to_html).join.freeze unless @items.empty?
27
+ end
28
+
29
+ def to_gemtext
30
+ @items.map(&:to_gemtext).join("\n").freeze unless @items.empty?
31
+ end
32
+
33
+ ###################
34
+ # Builder methods #
35
+ ###################
36
+
37
+ def blockquote
38
+ blockquote = Blockquote.new self
39
+ @items << blockquote
40
+ yield blockquote
41
+ nil
42
+ end
43
+
44
+ def code_block(*args, **kwargs)
45
+ code_block = CodeBlock.new self, *args, **kwargs
46
+ @items << code_block
47
+ nil
48
+ end
49
+
50
+ def iframe(*args, **kwargs)
51
+ iframe = Iframe.new self, *args, **kwargs
52
+ @items << iframe
53
+ nil
54
+ end
55
+
56
+ def links_list
57
+ list = List.new self, links: true, ordered: false
58
+ @items << list
59
+ yield list
60
+ nil
61
+ end
62
+
63
+ def nice_figure(name, alt)
64
+ figure = Figure.new self, name, alt
65
+ @items << figure
66
+ yield figure if block_given?
67
+ nil
68
+ end
69
+
70
+ def nice_figures
71
+ figures = Figures.new self
72
+ @items << figures
73
+ yield figures
74
+ nil
75
+ end
76
+
77
+ def olist
78
+ list = List.new self, links: false, ordered: true
79
+ @items << list
80
+ yield list
81
+ nil
82
+ end
83
+
84
+ def paragraph
85
+ paragraph = Paragraph.new self
86
+ @items << paragraph
87
+ yield paragraph
88
+ nil
89
+ end
90
+
91
+ def separator
92
+ @items << Separator.new(self)
93
+ nil
94
+ end
95
+
96
+ def ulist
97
+ list = List.new self, links: false, ordered: false
98
+ @items << list
99
+ yield list
100
+ nil
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Repubmark
4
+ module Elems
5
+ class Caption < Base
6
+ include Joint::ForwardingBuilders
7
+
8
+ parents :Blockquote, :Figure, :Footnote, :ListItem, :Paragraph
9
+
10
+ def initialize(parent)
11
+ super parent
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 unless @items.empty?
23
+ end
24
+
25
+ def to_html
26
+ return if @items.empty?
27
+
28
+ [
29
+ '<span>',
30
+ *@items.map(&:to_html),
31
+ '</span>',
32
+ ].map { |s| "#{s}\n" }.join.freeze
33
+ end
34
+
35
+ def to_gemtext
36
+ @items.map(&:to_gemtext).join(' ').strip.freeze unless @items.empty?
37
+ end
38
+
39
+ ##################
40
+ # Helper methods #
41
+ ##################
42
+
43
+ def empty? = @items.empty?
44
+
45
+ ###################
46
+ # Builder methods #
47
+ ###################
48
+
49
+ def joint
50
+ joint = Joint.new self
51
+ yield joint
52
+ @items << joint
53
+ nil
54
+ end
55
+
56
+ def quote(str = nil)
57
+ quote = Quote.new self
58
+ case [!!str, block_given?]
59
+ when [true, false] then quote.text str
60
+ when [false, true] then yield quote
61
+ else
62
+ raise 'Invalid args'
63
+ end
64
+ @items << quote
65
+ nil
66
+ end
67
+ end
68
+ end
69
+ end