marko 0.1.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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/Gemfile +10 -0
  4. data/Gemfile.lock +22 -0
  5. data/README.md +159 -0
  6. data/Rakefile +12 -0
  7. data/exe/marko +20 -0
  8. data/lib/assets/demo/README.md +13 -0
  9. data/lib/assets/demo/src/fr/assemble.md +27 -0
  10. data/lib/assets/demo/src/fr/compile.md +25 -0
  11. data/lib/assets/demo/src/fr/markup.md +111 -0
  12. data/lib/assets/demo/src/fr/storage.md +16 -0
  13. data/lib/assets/demo/src/fr/treenode.md +34 -0
  14. data/lib/assets/demo/src/index.md +34 -0
  15. data/lib/assets/demo/src/intro.md +98 -0
  16. data/lib/assets/demo/src/ui/cli.md +26 -0
  17. data/lib/assets/demo/src/ui/gem.md +14 -0
  18. data/lib/assets/demo/src/ur/uc.create.project.md +8 -0
  19. data/lib/assets/demo/src/ur/uc.general.flow.md +14 -0
  20. data/lib/assets/init/README.md +61 -0
  21. data/lib/assets/init/Rakefile +100 -0
  22. data/lib/assets/init/tt/artifact.md.tt +3 -0
  23. data/lib/marko/artifact.rb +3 -0
  24. data/lib/marko/assembler.rb +82 -0
  25. data/lib/marko/cli.rb +121 -0
  26. data/lib/marko/compiler.rb +16 -0
  27. data/lib/marko/config.rb +20 -0
  28. data/lib/marko/gadgets/pluggable.rb +55 -0
  29. data/lib/marko/gadgets/sentry.rb +66 -0
  30. data/lib/marko/gadgets/service.rb +52 -0
  31. data/lib/marko/gadgets.rb +3 -0
  32. data/lib/marko/loader.rb +38 -0
  33. data/lib/marko/markup/compiler.rb +36 -0
  34. data/lib/marko/markup/decorator.rb +65 -0
  35. data/lib/marko/markup/macro.rb +176 -0
  36. data/lib/marko/markup/parser.rb +122 -0
  37. data/lib/marko/markup/storage.rb +100 -0
  38. data/lib/marko/markup/validator.rb +101 -0
  39. data/lib/marko/markup.rb +24 -0
  40. data/lib/marko/parser.rb +19 -0
  41. data/lib/marko/services/assemble.rb +16 -0
  42. data/lib/marko/services/compile.rb +30 -0
  43. data/lib/marko/services.rb +2 -0
  44. data/lib/marko/storage.rb +36 -0
  45. data/lib/marko/tree_node.rb +128 -0
  46. data/lib/marko/validator.rb +19 -0
  47. data/lib/marko/version.rb +5 -0
  48. data/lib/marko.rb +37 -0
  49. data/marko.gemspec +44 -0
  50. metadata +99 -0
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
5
+ module Marko
6
+ module Markup
7
+
8
+ class Decorator < SimpleDelegator
9
+
10
+ def initialize(obj)
11
+ super(obj)
12
+ @macroproc = MacroProcPlug.plugged
13
+ end
14
+
15
+ def find_node(ref)
16
+ obj = super(ref)
17
+ return nil unless obj
18
+ self.class.new(obj)
19
+ end
20
+
21
+ def url
22
+ id.downcase
23
+ .gsub(/\W{1,}/, '-')
24
+ .gsub(/^-/, '')
25
+ .gsub(/-$/, '')
26
+ .then{"##{_1}"}
27
+ end
28
+
29
+ def ref
30
+ "[#{title}](#{url})"
31
+ end
32
+
33
+ def title
34
+ str = super
35
+ str = ".#{id.split(/\./).last}" if str.empty?
36
+ str
37
+ end
38
+
39
+ def header
40
+ return "% #{title}\n" if root?
41
+ "#{'#' * nesting_level} #{title.strip} {#{url}}\n"
42
+ end
43
+
44
+ def meta
45
+ hsh = super.dup
46
+ hsh[:id] = id # full id will be there
47
+ hsh.delete(:order_index)
48
+ hsh.delete(:parent)
49
+ hsh.delete(:origin)
50
+ len = hsh.keys.map(&:length).max
51
+ [].tap{|ary|
52
+ ary << "key | value"
53
+ ary << "--- | -----"
54
+ hsh.each{|k,v| ary << "#{k} | #{v}"}
55
+ }.join(?\n) + ?\n
56
+ end
57
+
58
+ def body
59
+ text = @macroproc.process(super, self)
60
+ text.strip + ?\n
61
+ end
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "decorator"
4
+ require_relative "../gadgets"
5
+
6
+ module Marko
7
+ module Markup
8
+
9
+ # @todo confusing circular depenencies
10
+ # List/Tree/Link Macros -> Decorator
11
+ # Decorator#body -> MacroProcessor
12
+
13
+ # Base class for macro substitutions
14
+ # @example
15
+ # class Todo < Macro
16
+ # @pattern = /@@todo[^\n]*\n/
17
+ # def subs(sample, obj = nil)
18
+ # # code that returns <substitution>
19
+ # # for the sample parameter
20
+ # end
21
+ # end
22
+ #
23
+ # text = "bla-bla-bla @@todo foo\nbla-bla-bla"
24
+ # Todo.new.gsub!(text)
25
+ # # => "bla-bla-bla <substitution for @@todo foo>"
26
+ #
27
+ class Macro
28
+ def self.pattern
29
+ @pattern
30
+ end
31
+
32
+ # @return [Regexp] pattern to process
33
+ def pattern
34
+ self.class.pattern
35
+ end
36
+
37
+ # substitutes all occured #pattern it text
38
+ def gsub!(text, obj = nil)
39
+ fn = subfn(text, obj)
40
+ text.scan(pattern).each(&fn)
41
+ end
42
+
43
+ protected
44
+
45
+ # build substitution for the text sample
46
+ # @param sample [String] sample for substitution
47
+ # @param obj [Object] might be used for substitution
48
+ # @return [String] substitution for text
49
+ def subs(sample, obj)
50
+ fail '#subs must be overridden'
51
+ end
52
+
53
+ def subfn(source, obj)
54
+ fn = proc{|source, obj, sample|
55
+ source.sub!(sample, subs(sample, obj))
56
+ }.curry
57
+ fn.(source, obj)
58
+ end
59
+ end
60
+
61
+ class MLink < Macro
62
+ @pattern = /\[\[[\w\.]*\]\]/
63
+
64
+ # the macro requires obj [Decorator]
65
+ def subs(sample, node)
66
+ capture = /\[\[([\w\.]*)\]\]/
67
+ ref = sample
68
+ .match(capture)
69
+ .captures.first
70
+ obj = node.find_node(ref)
71
+ obj ? obj.ref : "[#{ref}](#lost-link)"
72
+ end
73
+ end
74
+
75
+ class MList < Macro
76
+ @pattern = /@@list/
77
+
78
+ def subs(sample, node)
79
+ # @todo require sentry Decorator
80
+ # MustbeTreeNode.(node)
81
+ node.items
82
+ .map{|n| d = Decorator.new(n); "- #{d.ref}" }
83
+ .join(?\n) + ?\n
84
+ end
85
+ end
86
+
87
+ class MTree < Macro
88
+ @pattern = /@@tree/
89
+
90
+ def subs(sample, node)
91
+ level = node.nesting_level + 1
92
+ node.to_a.drop(1)
93
+ .inject([]){|memo, n| memo << Decorator.new(n)}
94
+ .map{|n| "#{' ' * (n.nesting_level - level)}- #{n.ref}" }
95
+ .join(?\n) + ?\n
96
+ end
97
+ end
98
+
99
+ # @todo there is no sense to have such macro for releases
100
+ # instead it might be helpful to have some custom
101
+ # todo command that will create a file with nodes
102
+ # and list of todo for each node
103
+ #
104
+ # class MTodo < Macro
105
+ # @pattern = /@@todo[^\n]*\n/
106
+ #
107
+ # def subs(sample, obj = nil)
108
+ # capture = /@@todo([^\n]*)\n/
109
+ # payload = sample.match(capture)
110
+ # .captures
111
+ # .first
112
+ # .strip
113
+ # "__TODO__ #{payload}\n"
114
+ # end
115
+ # end
116
+
117
+ # inline @@todo macro
118
+ # @todo remove line with \n when it starts from @@todo
119
+ class MTodo < Macro
120
+ @pattern = /.*@@todo.*$/
121
+
122
+ def subs(sample, obj = nil)
123
+ cap = /(.*)@@todo.*$/
124
+ m = sample.match(cap)
125
+ m[1].strip + sample.gsub(pattern, '')
126
+ end
127
+ end
128
+
129
+ class MSkip < Macro
130
+ @pattern = /@@skip.*$/m
131
+
132
+ def subs(sample, obj = nil)
133
+ ''
134
+ end
135
+ end
136
+
137
+ MustbeMacro = Sentry.new(:macro, "must be Macro"
138
+ ) {|v| v.is_a? Macro }
139
+
140
+ # Macro processor
141
+ # @example
142
+ # processor = MacroProcessor.new
143
+ # processor << Toc.new
144
+ # processor << Todo.new
145
+ # processor.('bla bla @@todo, bla bla @@list')
146
+ # # => 'bla bla <substitute @@todo>, bla bla..'
147
+ class MacroProcessor
148
+ extend Marko::Pluggable
149
+
150
+ def initialize
151
+ @macros = {}
152
+ self.<<(MList.new)
153
+ self.<<(MTree.new)
154
+ self.<<(MLink.new)
155
+ self.<<(MTodo.new)
156
+ self.<<(MSkip.new)
157
+ end
158
+
159
+ def <<(macro)
160
+ MustbeMacro.(macro)
161
+ @macros[macro.pattern] = macro
162
+ macro
163
+ end
164
+
165
+ def process(text, obj)
166
+ fail 'No macro registered' unless @macros.any?
167
+ String.new(text).tap {|str|
168
+ @macros.values.each{|m| m.gsub!(str, obj) }
169
+ }
170
+ end
171
+
172
+ alias :call :process
173
+ end
174
+
175
+ end
176
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../parser"
4
+ require_relative "../tree_node"
5
+
6
+ module Marko
7
+ module Markup
8
+
9
+ class Parser < Marko::Parser
10
+
11
+ # @see Parser#call
12
+ def call(content, source, &block)
13
+ @source = source
14
+ parse(content, &block)
15
+ end
16
+
17
+ protected
18
+
19
+ def parse(content, &block)
20
+ result, errors = [], [], []
21
+ scan_treenode(content)
22
+ .map{|origin|
23
+ begin
24
+ parse_treenode(origin.markup)
25
+ .tap{|n| n[:origin] = origin }
26
+ rescue => e
27
+ errmsg = "wrong markup #{origin} #{e.message}\n"
28
+ errors << errmsg
29
+ nil
30
+ end
31
+ }
32
+ .compact
33
+ .each{|node|
34
+ parent = find_parent(result, node[:origin].level)
35
+ unless parent
36
+ origin = node[:origin].to_s
37
+ errors << "wrong header #{origin}\n"
38
+ parent = result # it goes to the root!
39
+ end
40
+ parent << node
41
+ }
42
+
43
+ [result, errors]
44
+ end
45
+
46
+ def find_parent(ary, level)
47
+ return ary if level == 1
48
+ parent = ary.last
49
+ l = 1
50
+ while parent && (l+1) < level
51
+ parent = parent.last
52
+ l += 1
53
+ end
54
+ parent
55
+ end
56
+
57
+ Origin = Struct.new(:origin, :lineno, :markup) do
58
+ def header
59
+ @header ||= markup.lines.first
60
+ end
61
+
62
+ def level
63
+ @level ||= header.scan(/^#*/).first.size
64
+ end
65
+
66
+ def to_s
67
+ "#{origin}:#{lineno.to_s.rjust(2,'0')} >> #{header}".strip
68
+ end
69
+ end
70
+
71
+ # @return [Array<Origin>]
72
+ def scan_treenode(text)
73
+ quote, buffer, lineno = false, [], 0
74
+ origin = proc{
75
+ Origin.new(@source, lineno - buffer.size + 1, buffer.join(''))
76
+ }
77
+
78
+ [].tap{|ary|
79
+ text.each_line do |line|
80
+ if line =~ /^#/ && !quote && buffer.any?
81
+ ary << origin.()
82
+ buffer.clear
83
+ end
84
+ lineno += 1
85
+ buffer << line
86
+ quote = !quote if line.start_with?('```')
87
+ end
88
+ ary << origin.() if buffer.any?
89
+ }
90
+ end
91
+
92
+ def parse_treenode(text)
93
+ head, *tail = text.lines
94
+ m = head.match(/^(#+)(.*)/)
95
+ title = m[2]&.strip || ''
96
+
97
+ m = tail.join.match(/^({{([\s\S]*?)}})?(.*)?$/m)
98
+ meta = parse_metadata(m[2]&.strip || '')
99
+ body = m[3]&.strip || ''
100
+
101
+ TreeNode.new(title, body, **meta)
102
+ end
103
+
104
+ def parse_metadata(text)
105
+ return {} if text.empty?
106
+ atrbfn = method(:parse_attribute).to_proc
107
+ text
108
+ .split(/[;,\n]/)
109
+ .map(&:strip)
110
+ .reject(&:empty?)
111
+ .map(&atrbfn)
112
+ .to_h
113
+ end
114
+
115
+ def parse_attribute(text)
116
+ atr, val = text.split(?:)
117
+ [atr.strip.to_sym, val&.strip || 'true']
118
+ end
119
+ end
120
+
121
+ end
122
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+ require "psych"
3
+ require 'fileutils'
4
+ require "securerandom"
5
+ require_relative "../storage"
6
+
7
+ module Marko
8
+ module Markup
9
+
10
+ # File Storage
11
+ class Storage < Marko::Storage
12
+ include FileUtils
13
+
14
+ Failure = Class.new(StandardError)
15
+
16
+ # @see Storage#create
17
+ def punch(storage)
18
+ fail Failure, "Directory already exists" if Dir.exist?(storage)
19
+ Dir.mkdir(storage)
20
+ Dir.chdir(storage) {
21
+ marko_directories.each{|dir| Dir.mkdir dir }
22
+ src = File.join(Marko.root, 'lib', 'assets', 'init', '.')
23
+ cp_r src, Dir.pwd
24
+ }
25
+ end
26
+
27
+ # create demo project
28
+ def punch_demo
29
+ demo = File.join(Dir.home, DEMO)
30
+ unless Dir.exist?(demo)
31
+ punch(demo)
32
+ assets = File.join(Marko.root, 'lib', 'assets', 'demo', '.')
33
+ cp_r assets, demo
34
+ end
35
+ demo
36
+ end
37
+
38
+ # @see Storage#sources
39
+ def sources
40
+ marko_home!
41
+ ptrn = File.join(SOURCE, '**', '*.md')
42
+ Dir.glob ptrn
43
+ end
44
+
45
+ # @see Storage#content
46
+ def content(source)
47
+ marko_home!
48
+ File.read(source)
49
+ end
50
+
51
+ def write(filename, content = '')
52
+ backup(filename)
53
+ File.open(filename, 'w') do |f|
54
+ f.puts(content) unless content.empty?
55
+ yield(f) if block_given?
56
+ end
57
+ end
58
+
59
+ def marko_home?
60
+ marko_directories.all?{ Dir.exist? _1 }
61
+ end
62
+
63
+ # @see Marko::Strorage#artifact
64
+ def artifact
65
+ return Psych.load_file(ARTIFACT).freeze if File.exist?(ARTIFACT)
66
+ art = Artifact.new(SecureRandom.uuid,
67
+ 'Marko Artifact',
68
+ 'tt/artifact.md.tt',
69
+ 'tt/marko-artifact.md'
70
+ )
71
+ File.write(ARTIFACT, Psych.dump(art))
72
+ art.freeze
73
+ end
74
+
75
+ protected
76
+
77
+ ARTIFACT = 'marko.yml'.freeze
78
+ SOURCE = 'src'.freeze
79
+ BINARY = 'bin'.freeze
80
+ SAMPLE = 'tt'.freeze
81
+ ASSETS = File.join(BINARY, 'assets').freeze
82
+ DEMO = 'marko_demo'.freeze
83
+
84
+ def marko_directories
85
+ [SOURCE, BINARY, ASSETS, SAMPLE]
86
+ end
87
+
88
+
89
+ def marko_home!
90
+ fail Failure, "Marko project required!" unless marko_home?
91
+ end
92
+
93
+ def backup(filename)
94
+ cp(filename, filename + ?~) if File.exist?(filename)
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../validator"
4
+
5
+ module Marko
6
+ module Markup
7
+ class Validator < Marko::Validator
8
+
9
+ def initialize
10
+ @inspectors = []
11
+ @inspectors << TheSameId.new
12
+ @inspectors << LostParent.new
13
+ @inspectors << LostIndex.new
14
+ @inspectors << LostLink.new
15
+ end
16
+
17
+ # see Marko::Validator#call
18
+ def call(tree)
19
+ [].tap{|errors|
20
+ @inspectors.each{|spectr| errors.concat(spectr.(tree)) }
21
+ }
22
+ end
23
+
24
+ end
25
+
26
+ class Inspector
27
+ def call(tree)
28
+ select(tree).then{ report(_1) }
29
+ end
30
+
31
+ def select(tree)
32
+ fail "the abstract method must be overriden"
33
+ end
34
+
35
+ def report(errs)
36
+ fail "the abstract method must be overriden"
37
+ end
38
+
39
+ end
40
+
41
+ class TheSameId < Inspector
42
+ def select(tree)
43
+ fn = proc{|n, memo| memo[n.id] ||= []; memo[n.id] << n}
44
+ tree
45
+ .each_with_object({}, &fn)
46
+ .select{|_, v| v.size > 1}
47
+ end
48
+
49
+ def report(errs)
50
+ # the same id [ref] found twice
51
+ # src/source2.md:22 >> ## header
52
+ # src/source3.md:11 >> ## header
53
+ errs.map{|id, nodes|
54
+ sources = nodes.map{|n| " #{n[:origin]}\n"}.join
55
+ "the same id [#{id}] found in\n#{sources}"
56
+ }
57
+ end
58
+ end
59
+
60
+ class LostParent < Inspector
61
+ def select(tree)
62
+ tree.select{|n| n[:parent] && n.parent_id != n[:parent]}
63
+ end
64
+
65
+ def report(errs)
66
+ # lost parent [ref] src/source2.md:22 >> ## header
67
+ errs.map{|n| "lost parent [#{n[:parent]}] found in #{n[:origin]}\n"}
68
+ end
69
+ end
70
+
71
+ class LostIndex < Inspector
72
+ def select(tree)
73
+ lost = proc{|n| n.order_index.reject{|i| n.find_item(i)}}
74
+ tree
75
+ .select{|n| n.order_index.any? && lost.(n).any?}
76
+ .map{|n| [n[:origin], lost.(n)]} # @todo second time calculation
77
+ end
78
+
79
+ def report(errs)
80
+ # lost index [a, b] src/source2.md:22 >> ## header
81
+ errs.map{|orig, lost| "lost index [#{lost.join(', ')}] in #{orig}\n" }
82
+ end
83
+ end
84
+
85
+ class LostLink < Inspector
86
+ def select(tree)
87
+ lost = proc{|n| n.links.reject{|i| n.find_node(i)}}
88
+ tree
89
+ .select{|n| lost.(n).any? }
90
+ .map{|n| [n[:origin], lost.(n)]} # @todo second time #lost calculation
91
+ end
92
+
93
+ # @todo report link line number in the origin
94
+ def report(errs)
95
+ # lost links [ref] src/source2.md:22 >> ## header
96
+ errs.map{|orig, lost| "lost link [#{lost.join(', ')}] in #{orig}\n"}
97
+ end
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "markup/parser"
2
+ require_relative "markup/storage"
3
+ require_relative "markup/validator"
4
+ require_relative "markup/macro"
5
+ require_relative "markup/decorator"
6
+ require_relative "markup/compiler"
7
+ require_relative "config"
8
+
9
+
10
+ module Marko
11
+
12
+ ParserPlug.plug Markup::Parser
13
+ StoragePlug.plug Markup::Storage
14
+ CompilerPlug.plug Markup::Compiler
15
+ ValidatorPlug.plug Markup::Validator
16
+
17
+ # ParserPlug = Markup::Parser.plug
18
+ # StoragePlug = Markup::Storage.plug
19
+ # @todo let Decorator know
20
+ MacroProcPlug = Markup::MacroProcessor.plug
21
+ # ValidatorPlug = Markup::Validator.plug
22
+ # CompilerPlug = Markup::Compiler.plug
23
+
24
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gadgets"
4
+
5
+ module Marko
6
+
7
+ # The class for pasing content into TreeNode
8
+ class Parser
9
+ extend Pluggable
10
+
11
+ # @param content [String] content to parse
12
+ # @param source [String] content source
13
+ # @return [Array<TreeNode>, Array<String>] parsed nodes, errors
14
+ def call(content, source, &block)
15
+ fail "the abstract method must be overriden"
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,16 @@
1
+ require_relative "../gadgets"
2
+ require_relative "../assembler"
3
+
4
+ module Marko
5
+ module Services
6
+
7
+ # Assemblage service
8
+ # @todo assemble projects bu url
9
+ class Assemble < Service
10
+ def call
11
+ Assembler.(&@block)
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ require_relative "../gadgets"
2
+ require_relative "../assembler"
3
+
4
+ module Marko
5
+ module Services
6
+
7
+ # Compitation service
8
+ class Compile < Service
9
+ def initialize(tree: nil, template: '', filename: '', &block)
10
+ @tree = MustbeTreeNode.(tree) if tree
11
+ @template = MustbeString.(template)
12
+ @filename = MustbeString.(filename)
13
+ @block = block
14
+
15
+ art = Marko.artifact
16
+ @template = art.template if @template.empty?
17
+ @filename = art.filename if @filename.empty?
18
+ end
19
+
20
+ def call
21
+ storage = StoragePlug.plugged
22
+ compiler = CompilerPlug.plugged
23
+ erb = storage.content(@template)
24
+ @tree = Assembler.(&@block) unless @tree
25
+ compiler.(@tree, erb, @filename, &@block) # => filename
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "services/assemble"
2
+ require_relative "services/compile"
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gadgets"
4
+ require_relative "artifact"
5
+
6
+ module Marko
7
+
8
+ # The base class that represents sources repository
9
+ class Storage
10
+ extend Pluggable
11
+
12
+ # create a new repository
13
+ # @param repository [String] the name for the repository
14
+ def punch(repository)
15
+ fail "the abstract method must be overriden"
16
+ end
17
+
18
+ # @retrun [Array<String>] array of sources inside the repository
19
+ def sources
20
+ fail "the abstract method must be overriden"
21
+ end
22
+
23
+ # @param source [Striing] source to retrieve content
24
+ # @return [String] content of :source
25
+ def content(source)
26
+ fail "the abstract method must be overriden"
27
+ end
28
+
29
+ # @return [Artifact] artifact settings
30
+ def artifact
31
+ fail "the abstract method must be overriden"
32
+ end
33
+
34
+ end
35
+
36
+ end