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,61 @@
1
+ % Marko Demo Project
2
+
3
+ Welcome to your new [Marko](https://github.com/nvoynov/clerq) project!
4
+
5
+ # Structure
6
+
7
+ This project has the following structure:
8
+
9
+ - [bin/](bin/) - output folder
10
+ - [bin/assets/](bin/assets/) - assets folder
11
+ - [src/](src/) - markup sources
12
+ - [tt/](tt/) - templates
13
+ - [marko.yml](marko.yml) - project configuration
14
+ - [Rakefile](Rakefile) - Rakefile
15
+ - [README.md](README.md)
16
+
17
+ # Interface
18
+
19
+ Run `marko` command in your console to see basic command-line interface.
20
+
21
+ Extend it yourself through Rakefile
22
+
23
+ # Git Repository
24
+
25
+ [Git How To](https://githowto.com/)
26
+
27
+ Incorporates changes from a remote repository:
28
+
29
+ git pull
30
+
31
+ Create a new branch to start any activity with the repository:
32
+
33
+ git branch <branch_name>
34
+
35
+ Make changes and commit your work:
36
+
37
+ git add .
38
+ git commit -m "branch_name - commit description"
39
+
40
+ When your changes finished, incorporate changes from the remote repository and merge the `master` branch to your `branch_name`:
41
+
42
+ git checkout master
43
+ git pull
44
+ git checkout <branch_name>
45
+ git merge master
46
+
47
+ Resolve all conflicts and commit changes:
48
+
49
+ git add .
50
+ git commit -m "branch_name conflicts resolved"
51
+
52
+ Merge your changes to the `master` branch:
53
+
54
+ git checkout master
55
+ git merge <branch_name>
56
+
57
+ Push your changes to the remote repository:
58
+
59
+ git push
60
+
61
+ Create a merge request if you are not allowed to push to the `master` branch.
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'marko'
4
+ require 'marko/markup'
5
+
6
+ namespace :marko do
7
+
8
+ # @todo pandoc: bin/arfifact.docx: openBinaryFile: permission denied (Permission denied)
9
+ # Success! Find the artifact in bin/arfifact.docx
10
+ #
11
+ desc 'Publish the artifact'
12
+ task :publish do
13
+ output = 'bin/arfifact'
14
+ `marko c -o #{output}.md`
15
+ pandoc = `pandoc #{output}.md -o #{output}.docx`
16
+ File.delete("#{output}.md")
17
+ puts "Success! Find the artifact in #{output}.docx"
18
+ end
19
+
20
+ desc 'Print table of contents'
21
+ task :toc, :query do |t, args|
22
+ args.with_defaults(query: '')
23
+ tree = Marko.assemble
24
+ query = args.query
25
+ unless query.empty?
26
+ tree = tree.find_node(query)
27
+ unless tree
28
+ puts "Nothing to be printed!"
29
+ return
30
+ end
31
+ tree.orphan!
32
+ end
33
+ tree.each do |n|
34
+ if n.root?
35
+ puts "% #{n.title}" if n.root?
36
+ next
37
+ end
38
+ title = n.title.empty? ? n.id : n.title
39
+ puts '#' * n.nesting_level + " #{title} ##{n.id}"
40
+ end
41
+ end
42
+
43
+ desc 'Print @@todo'
44
+ task :todo do
45
+ tree = Marko.assemble
46
+ tree.to_a.drop(1)
47
+ .select{|n| n.body =~ /@@todo/}
48
+ .each{|n|
49
+ list = n.body
50
+ .scan(/@@todo.*$/)
51
+ .map{_1.sub(/@@todo/, '')}
52
+ .map{" #{_1.strip}"}
53
+ .join(?\n)
54
+ puts n[:origin]
55
+ puts list
56
+ puts
57
+ }
58
+ end
59
+
60
+ def datestamp
61
+ Time.new.strftime('%Y%m%d')
62
+ end
63
+
64
+ def timestamp
65
+ Time.new.to_s
66
+ end
67
+
68
+ desc 'Punch meeting munutes'
69
+ task :mm, :extra do |t, args|
70
+ args.with_defaults(extra: '')
71
+ extra = args.extra
72
+ Dir.mkdir('mm') unless Dir.exist?('mm')
73
+ body = MINUTES % timestamp
74
+ name = "minutes-#{datestamp}#{extra.empty? ? '' : ?_ + extra}"
75
+ name = File.join('mm', name)
76
+ errm = "Minutes exists already. Maybe you can provide [EXTRA]?"
77
+ fail errm if File.exist?(name)
78
+ File.write(name, body)
79
+ end
80
+
81
+ MINUTES = <<~EOF.freeze
82
+ %% Meeting Minutes %s
83
+
84
+ # Attendants
85
+
86
+ 1.
87
+ 2.
88
+
89
+ # Questions
90
+
91
+ 1.
92
+ 2.
93
+
94
+ # Decisions
95
+
96
+ 1.
97
+ 2.
98
+ EOF
99
+
100
+ end
@@ -0,0 +1,3 @@
1
+ <%= @node.header %>
2
+ <%= @node.meta %>
3
+ <%= @node.body %>
@@ -0,0 +1,3 @@
1
+ module Marko
2
+ Artifact = Struct.new(:id, :title, :template, :filename)
3
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gadgets"
4
+ require_relative "config"
5
+ require_relative "loader"
6
+ require_relative "tree_node"
7
+
8
+ module Marko
9
+
10
+ # The strategy for assembling sources into artifact tree
11
+ class Assembler < Service
12
+
13
+ class Failure # < StandardError
14
+ attr_reader :errors
15
+ def initialize(message, *errors)
16
+ @errors = errors
17
+ super(
18
+ errors
19
+ .map{|e| e.lines.map{|l| " #{l}"}.join }
20
+ .unshift(message + ?\n)
21
+ .join
22
+ )
23
+ end
24
+ end
25
+
26
+ # @return [TreeNode]
27
+ def call
28
+ @block.(:stage, 'loading sources') if @block
29
+ buffer, errors = Loader.(&@block)
30
+ fail Failure.new('markup parsing errors', *errors) if errors.any?
31
+ @block.(:stage, 'tree assemblage') if @block
32
+ tree = assemble(buffer)
33
+ @block.(:stage, 'tree enrichment') if @block
34
+ injectid(tree)
35
+ @block.(:stage, 'tree validation') if @block
36
+ errors = validate(tree)
37
+ fail Failure.new('tree validation errors', *errors) if errors.any?
38
+ tree
39
+ end
40
+
41
+ protected
42
+
43
+ # @param buff [Array<TreeNode>]
44
+ # @return [TreeNode]
45
+ def assemble(buff)
46
+ art = Marko.artifact
47
+ TreeNode.new(art.title, id: '0').tap {|root|
48
+ # @todo buff.inject(root, :<<) wrong but why?
49
+ buff.each{|n| root << n}
50
+ root.items
51
+ .select{|node| node[:parent] && node[:parent] != node.parent_id}
52
+ .each{|node|
53
+ parent = root.find{|n| n.id == node[:parent]}
54
+ next unless parent # is must be left under root
55
+ parent << node
56
+ node.delete_meta(:parent)
57
+ root.delete_item(node)
58
+ }
59
+ }
60
+ end
61
+
62
+ def injectid(tree)
63
+ counter = {}
64
+ tree
65
+ .select {|node| node.id.empty?}
66
+ .each do |node|
67
+ index = counter[node.parent] || 1
68
+ counter[node.parent] = index + 1
69
+ id = index.to_s.rjust(2, '0')
70
+ id = '.' + id unless node.parent == tree
71
+ node[:id] = id
72
+ end
73
+ end
74
+
75
+ def validate(tree)
76
+ validator = ValidatorPlug.plugged
77
+ validator.(tree)
78
+ end
79
+
80
+ end
81
+
82
+ end
data/lib/marko/cli.rb ADDED
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "services"
5
+ include Marko::Services
6
+
7
+ module Marko
8
+ module CLI
9
+ extend self
10
+
11
+ def punch(dir, args = ARGV)
12
+ if Dir.exist?(dir)
13
+ puts "Directory '#{dir}' already exist!"
14
+ return
15
+ end
16
+ storage.punch(dir)
17
+ puts "Marko directory created!"
18
+ log = Dir.chdir(dir) { Dir.glob('**/*') }
19
+ puts log.map{ " created #{dir}/#{_1}" }.join(?\n)
20
+ kwargs = options(:new, args)
21
+ editor = kwargs.fetch(:editor, '')
22
+ Dir.chdir(dir) { `#{editor} .` } unless editor.empty?
23
+ end
24
+
25
+ def punch_demo(args = ARGV)
26
+ # pp options(:demo, args)
27
+ log = storage.punch_demo
28
+ puts "Marko Demo cloned!\n#{log}"
29
+ kwargs = options(:demo, args)
30
+ editor = kwargs.fetch(:editor, '')
31
+ Dir.chdir(log) { `#{editor} .` } unless editor.empty?
32
+ end
33
+
34
+ def compile(args = ARGV)
35
+ unless storage.marko_home?
36
+ puts "Marko directory required!"
37
+ return
38
+ end
39
+ kwargs = options(:compile, args)
40
+ verbose = kwargs.delete(:verbose) { false }
41
+ pulsefn = method(verbose ? :pulse_verbose : :pulse_concise)
42
+ result = Compile.(**kwargs, &pulsefn)
43
+ puts "compiled #{result}\nSuccess!"
44
+ rescue Marko::Assembler::Failure => e
45
+ puts "Assembler failed with #{e.errors.size} #{e.message}"
46
+ puts "Failure"
47
+ end
48
+
49
+ def banner
50
+ puts "#{BANNER}\nUsage:"
51
+ PARSER.each{|_, cmd| puts "\n #{cmd}"}
52
+ end
53
+
54
+ protected
55
+
56
+ def storage
57
+ StoragePlug.plugged
58
+ end
59
+
60
+ def pulse_verbose(events, payload)
61
+ case events
62
+ when :stage
63
+ puts "#{payload}.."
64
+ when :source
65
+ print " #{payload}.."
66
+ when :errors
67
+ unless payload.empty?
68
+ puts "#{payload.size} errors #{payload.join}"
69
+ else
70
+ puts "OK"
71
+ end
72
+ end
73
+ end
74
+
75
+ def pulse_concise(events, payload)
76
+ case events
77
+ when :stage
78
+ puts "#{payload}.."
79
+ end
80
+ end
81
+
82
+ def options(cmd, args = ARGV)
83
+ {}.tap{|opt| PARSER[cmd].parse(args, into: opt)}
84
+ end
85
+
86
+ # \ ^__^
87
+ # \ (oo)\_______
88
+ # (__)\ )\/\
89
+ # ||----w |
90
+ # || ||
91
+ BANNER = <<~EOF.freeze
92
+ ~ Marko v0.1.3 ~ Markup Compiler
93
+ ~ https://github.io/nvoynov/marko
94
+ EOF
95
+
96
+ PARSER = {
97
+ new: OptionParser.new {|cmd|
98
+ ban = "marko new|n DIRECTORY [OPTIONS]".ljust(35, ?\s)
99
+ cmd.banner = ban + "Create a new Marko project"
100
+ cmd.on('-e EDITOR', '--editor EDITOR',
101
+ 'Open the project in EDITOR', String)
102
+ },
103
+ demo: OptionParser.new {|cmd|
104
+ ban = "marko demo|d [OPTIONS]".ljust(35, ?\s)
105
+ cmd.banner = ban + "Clone Marko Demo project"
106
+ cmd.on('-e EDITOR', '--editor EDITOR',
107
+ 'Open the project in EDITOR', String)
108
+ },
109
+ compile: OptionParser.new {|cmd|
110
+ ban = "marko compile|c [OPTIONS]".ljust(35, ?\s)
111
+ cmd.banner = ban + "Compile Marko artifact"
112
+ cmd.on('-t TEMPLATE', '--template TEMPLATE',
113
+ 'Template to render', String)
114
+ cmd.on('-o FILENAME', '--filename FILENAME',
115
+ 'Render to filename', String)
116
+ cmd.on('-v', '--verbose', 'Run verbosely')
117
+ }
118
+ }.freeze
119
+
120
+ end
121
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tree_node"
4
+ require_relative "gadgets"
5
+
6
+ module Marko
7
+ class Compiler
8
+ extend Pluggable
9
+ def call(tree, template, filename, &block)
10
+ @tree = MustbeTreeNode.(tree)
11
+ @template = MustbeString.(template)
12
+ @filename = MustbeString.(filename)
13
+ @block = block
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+
2
+ require_relative "parser"
3
+ require_relative "storage"
4
+ require_relative "compiler"
5
+ require_relative "validator"
6
+
7
+ module Marko
8
+
9
+ # default abstract starters that must be replugged later
10
+ ParserPlug = Parser.plug
11
+ StoragePlug = Storage.plug
12
+ CompilerPlug = Compiler.plug
13
+ ValidatorPlug = Validator.plug
14
+
15
+ def self.artifact
16
+ storage = StoragePlug.plugged
17
+ storage.artifact
18
+ end
19
+
20
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marko
4
+ # Pluggable mixin serves for dependency injection
5
+ #
6
+ # @example
7
+ # class Storage
8
+ # extend Pluggable
9
+ # def call
10
+ # end
11
+ # end
12
+ #
13
+ # StoragePort = Storage.port
14
+ #
15
+ # class SequelStorage < Storage
16
+ # end
17
+ #
18
+ # StoragePort.plugged = SequelStorage.new
19
+ #
20
+ # require 'forwardable'
21
+ # class Service
22
+ # extend Forwardable
23
+ # def_delegator :StoragePort, :plugged, :storage
24
+ # def call
25
+ # storage.call
26
+ # end
27
+ # end
28
+ module Pluggable
29
+
30
+ def plug
31
+ klass = self
32
+ Module.new {
33
+ extend Plug;
34
+ plug klass
35
+ }
36
+ end
37
+
38
+ module Plug
39
+ def plug(klass)
40
+ @klass = klass
41
+ end
42
+
43
+ def plugged
44
+ fail "unknown @klass" unless @klass
45
+ @plugged ||= @klass.new
46
+ end
47
+
48
+ def plugged=(obj)
49
+ fail ArgumentError.new("required an instance of #{@klass}"
50
+ ) unless obj.is_a?(@klass)
51
+ @plugged = obj
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marko
4
+ # Factory module for guarding method arguments
5
+ #
6
+ # @example
7
+ #
8
+ # ShortString = Sentry.new(:str, "must be String[3..100]"
9
+ # ) {|v| v.is_a?(String) && v.size.between?(3,100)}
10
+ #
11
+ # ShortString.(str) => "str"
12
+ # ShortString.(nil) => ArgumentError ":str must be String[3..100]"
13
+ # ShortString.error(nil) => ":str must be String[3..100]"
14
+ # ShortString.error!(nil) => ArgumentError":str must be String[3..100]"
15
+ # ShortString.(nil, :name) => ArgumentError ":name must be String[3..100]"
16
+ # ShortString.(nil, 'John Doe', 'Ups!') => ArgumentError ":John Doe Ups!"
17
+ #
18
+ module Sentry
19
+
20
+ # creates a new Sentry
21
+ # @param key [Symbol|String] key for error message
22
+ # @param msg [String] error message
23
+ # @param blk [&block] validation block that should return boolen
24
+ # @return [Sentry] based on key, msg, and validation block
25
+ def self.new(key, msg, &blk)
26
+ # origin ;)
27
+ argerror = ->(val, msg, cnd) {
28
+ fail ArgumentError, msg unless cnd
29
+ val
30
+ }
31
+ Module.new do
32
+ include Sentry
33
+ extend self
34
+
35
+ @key = argerror.(key, ":key must be Symbol|String",
36
+ key.is_a?(String) || key.is_a?(Symbol))
37
+ @msg = argerror.(msg, ":msg must be String", msg.is_a?(String))
38
+ @blk = argerror.(blk, "&blk must be provided", block_given?)
39
+ end
40
+ end
41
+
42
+ # returns error message for invalid :val
43
+ # @param val [Object] value to be validated
44
+ # @param key [Symbol|String] key for error message
45
+ # @param msg [String] optional error message
46
+ # @return [String] error message for invalid :val or nil when :val is valid
47
+ def error(val, key = @key, msg = @msg)
48
+ ":#{key} #{msg}" unless @blk.(val)
49
+ end
50
+
51
+ # guards :val
52
+ # @todo @see Yard!
53
+ # @param val [Object] value to be returned if it valid
54
+ # @param key [Symbol|String] key for error message
55
+ # @param msg [String] optional error message
56
+ # @return [Object] valid :val or raieses ArgumentError when invalid
57
+ def error!(val, key = @key, msg = @msg)
58
+ return val if @blk.(val)
59
+ fail ArgumentError, ":#{key} #{msg}", caller[0..-1]
60
+ end
61
+
62
+ # @todo how do describe in Yard?
63
+ alias :call :error!
64
+ end
65
+
66
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marko
4
+ # Service like ServiceObject, Command, etc.
5
+ #
6
+ # @example just call without parameters
7
+ # class BoldService < Service
8
+ # def call
9
+ # 42
10
+ # end
11
+ # end
12
+ #
13
+ # @example with parameters
14
+ # class PlusService < Service
15
+ # def initialize(a, b)
16
+ # @a, @b = a, b
17
+ # end
18
+ #
19
+ # def call
20
+ # 42
21
+ # end
22
+ # end
23
+ #
24
+ class Service
25
+ Failure = Class.new(StandardError)
26
+
27
+ class << self
28
+ def call(*args, **kwargs, &block)
29
+ new(*args, **kwargs, &block).call
30
+ end
31
+
32
+ def inherited(klass)
33
+ klass.const_set(:Failure, Class.new(klass::Failure))
34
+ super
35
+ end
36
+ end
37
+
38
+ private_class_method :new
39
+
40
+ def initialize(*args, **kwargs, &block)
41
+ @args = args
42
+ @kwargs = kwargs
43
+ @block = block
44
+ end
45
+
46
+ def call
47
+ fail Failure, "#{self.class.name}#call must be overrided"
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "gadgets/sentry"
2
+ require_relative "gadgets/pluggable"
3
+ require_relative "gadgets/service"
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gadgets"
4
+ require_relative "config"
5
+
6
+ module Marko
7
+
8
+ # The strategy class for loading sources from repository
9
+ class Loader < Service
10
+ # load markup sources, parse and return TreeNode buffer
11
+ #
12
+ # @example
13
+ # fn = proc{|event, paylod| ... }
14
+ # buffer, errors = loader.(&fn)
15
+ # fail "Failed" if errors.any?
16
+ # # procced ...
17
+ #
18
+ # @param block [&block] aka proc {|event, payload| ..}
19
+ # @return [Array<TreeNode>, Array<String>] where
20
+ # the first item is buffer and the second is array<error>
21
+ def call
22
+ buffer = []
23
+ errors = []
24
+ parser = ParserPlug.plugged
25
+ storage = StoragePlug.plugged
26
+ storage.sources.each do |source|
27
+ @block.(:source, source) if @block
28
+ content = storage.content(source)
29
+ buff, errs = parser.(content, source, &@block)
30
+ buffer.concat(buff)
31
+ errors.concat(errs)
32
+ @block.(:errors, errs) if @block
33
+ end
34
+ [buffer.flatten, errors]
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require_relative "../compiler"
5
+ require_relative "../config"
6
+
7
+ module Marko
8
+ module Markup
9
+
10
+ class Compiler < Marko::Compiler
11
+
12
+ # @see Marko::Compliler#call
13
+ def call(tree, template, filename, &block)
14
+ super(tree, template, filename, &block)
15
+ compile
16
+ end
17
+
18
+ protected
19
+
20
+ def compile
21
+ storage = StoragePlug.plugged
22
+ erbgen = ERB.new(@template, trim_mode: '-')
23
+ payload = @tree.map{|n| Decorator.new(n)}
24
+ storage.write(@filename){|f|
25
+ payload.each{|node|
26
+ @node = node
27
+ text = erbgen.result(binding)
28
+ f.puts text
29
+ }
30
+ }
31
+ @filename
32
+ end
33
+ end
34
+
35
+ end
36
+ end