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