grokdown 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d40e626d89381d669dd420c80541f010aa0081f788fd5728feb40e5c2573a1a
4
- data.tar.gz: 312883afadca7036f18f42ef6f417b6d1f7117f7dabf174122200e392624ad7c
3
+ metadata.gz: ac8a98fe8b785069748b8188029679769d84f1a4b7b8cf92abe5168669e0f5dd
4
+ data.tar.gz: e447bf05e89b2726b9715984d1e74fbb756c00400173efda99da59cedcc53bb8
5
5
  SHA512:
6
- metadata.gz: eae64369ef2ad249bbc2d03e130e21da7977bd4157b5a813fb9032238fa2afae8b76f316add2aa330dd725f10a06bb9ae22be16d72f961a7c8677d36479bd90b
7
- data.tar.gz: 69e31f53ed7131294cd9f16940f7a6d1f74ae689f8177c6ad552e1751c58eecdce5799282ca2e88cf11c0bd44d3910c6296bf5d118513274166640732e7dd75a
6
+ metadata.gz: ddbc26e585b14064b760417a7b3108f385f7df479497fa2753ed22b24a613f8f9175427b540a41c217b85a5b0288782465de95076cc7d6b6ad42cd3f98818dd3
7
+ data.tar.gz: 18ffe9aa81977fe8c99c016ad95cd6bb77502ac85cc88c3f22848db0c945e43331af8b91dae20ee55fe6c4faa3e5fcdebb83474bf8ca8ac203bdf68ff9575f9c
data/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ # Todo
data/README.md CHANGED
@@ -1,24 +1,119 @@
1
- # Grokdown
1
+ # Grokdown: Markdown to Ruby Objects
2
2
 
3
- Grok Markdown documents with Ruby objects. Have fun using whats inside Markdown documents, without all of the pain of walking a document tree.
3
+ `Grokdown` provides **an experimental interface** for building value objects and composing them into entities from a Markdown document tree.
4
4
 
5
- ## Grok
5
+ ## Usage
6
6
 
7
- > - to understand intuitively or by empathy, to establish rapport with
8
- > - to empathize or communicate sympathetically (with); also, to experience
9
- > enjoyment
7
+ Include the `Grokdown` module into ruby classes you want `Grokdown::Document` to consider building value objects from.
10
8
 
11
- ## Markdown
9
+ `Grokdown::Document` depends on class methods `matches_node?` and `agruments_from_node` to select which `Grokdown` class to build and how to build an instance from a Markdown node.
12
10
 
13
- > Markdown allows you to write using an easy-to-read, easy-to-write plain text
14
- > format, then convert it to structurally valid XHTML (or HTML).
11
+ `Grokdown` instances can compose `Grokdown` value objects or entities by implementing instance hook methods following a naming convention. The hook method name is `add_` prefixing the snake case `Grokdown` class name of the instances a `Grokdown` instance can get added.
12
+
13
+ Implementing the hook methods creates precise and resilient factories for objects from Markdown documents.
14
+
15
+ Receiver | Hook method | Use case
16
+ --- | --- | ---
17
+ Class | `matches_node?` | Predicate to select receiving class to build from a given Markdown node|
18
+ Class | `arguments_from_node` | Maps node values to build an instance of the class from a given Markdown node |
19
+ Instance | `add_other_class_name` | Aggregate later `OtherClassName` instances when visiting the Markdown node tree |
20
+
21
+ Simple differences in implementations of `add_` composition methods enables building useful ruby object graphs from easy to write Markdown files.
22
+
23
+ ### Extracting License information from README.md
24
+
25
+ Create a `.grokdown` file which defines types to build from markdown nodes, then use the `grokdown` CLI to extract the license name with:
26
+
27
+ ```sh
28
+ grokdown -e "Document.new(File.read('README.md')).first.license.name"
29
+ ```
30
+
31
+ ##### `README.md`
32
+
33
+ ```
34
+ # Example Readme
35
+
36
+ Simple readme with a conventional `## License` section
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
41
+ ```
42
+
43
+ ##### `.grokdown`
44
+
45
+ ```ruby
46
+ require "grokdown"
47
+
48
+ class Text < String
49
+ include Grokdown
50
+
51
+ def self.matches_node?(node) = node.type == :text
52
+
53
+ def self.arguments_from_node(node) = node.string_content
54
+ end
55
+
56
+ Link = Struct.new(:href, :title, :text, :parent, keyword_init: true) do
57
+ include Grokdown
58
+
59
+ def self.matches_node?(node) = node.type == :link
60
+
61
+ def self.arguments_from_node(node) = {href: node.url, title: node.title}
62
+
63
+ def add_text(node)
64
+ return if text
65
+
66
+ self.text = node
67
+
68
+ parent.add_composable(node) if parent&.can_compose?(node)
69
+ end
70
+ end
71
+
72
+ License = Struct.new(:text, :href, :name, :link, keyword_init: true) do
73
+ include Grokdown
74
+
75
+ def self.matches_node?(node) = node.type == :header && node.header_level == 2 && node.first_child.string_content == "License"
76
+
77
+ def add_text(node) = self.text = node
78
+
79
+ extend Forwardable
80
+
81
+ def_delegator :link, :href
82
+
83
+ def add_link(node)
84
+ node.parent = self
85
+ self.link = node
86
+ end
87
+
88
+ def add_text(node) = self.name = node
89
+ end
90
+
91
+ Struct.new(:text, :link, :keyword_init) do
92
+ include Grokdown
93
+
94
+ def self.matches_node?(node) = node.type == :header && node.header_level == 2
95
+
96
+ def add_text(node) = self.text = node
97
+ def add_link(node) = self.link = node
98
+ end
99
+
100
+ Readme = Struct.new(:license) do
101
+ include Grokdown
102
+
103
+ def self.matches_node?(node) = node.type == :document
104
+
105
+ def add_license(node) = self.license = node
106
+ end
107
+ ```
15
108
 
16
109
  ## Installation
17
110
 
18
111
  Install the gem and add to the application's Gemfile by executing:
112
+
19
113
  $ bundle add grokdown
20
114
 
21
115
  If bundler is not being used to manage dependencies, install the gem by executing:
116
+
22
117
  $ gem install grokdown
23
118
 
24
119
  ## Development
data/exe/grokdown ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "forwardable"
4
+ require "optparse"
5
+
6
+ require "bundler/setup"
7
+
8
+ require "grokdown"
9
+ require "grokdown/document"
10
+
11
+ document_definition = Pathname.glob(".grokdown").find { _1.exist? }
12
+
13
+ load document_definition
14
+
15
+ parser = OptionParser.new
16
+
17
+ parser.on("-e [ruby expression]", "Ruby expression to evaluate after loading the Grokdown document definition") do |value|
18
+ puts Grokdown.module_eval(value)
19
+ end
20
+
21
+ parser.parse!
@@ -0,0 +1,21 @@
1
+ module Grokdown
2
+ module Composing
3
+ def self.extended(base) = base.include(InstanceMethods)
4
+
5
+ def can_compose?(object) = public_instance_methods.include?(composition_method(object))
6
+
7
+ def composition_method(object)
8
+ :"add_#{object.class.name.gsub(/#<.*>::/,"").gsub("::", "_").gsub(/([A-Z])(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { ($1 || $2) << "_" }.downcase}" if object.class.name
9
+ end
10
+
11
+ module InstanceMethods
12
+ def can_compose?(object) = self.class.can_compose?(object)
13
+
14
+ def composition_method(object) = self.class.composition_method(object)
15
+
16
+ def add_composable(object)
17
+ public_send(composition_method(object), object)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -6,25 +6,24 @@ module Grokdown
6
6
  base.send(:include, InstanceMethods)
7
7
  end
8
8
 
9
- def create(many: false, &block)
10
- @create = block
11
- @create_many = many
12
- end
13
-
14
9
  def build(node)
15
- if @create
16
- args = begin
17
- @create.call(node)
18
- rescue NoMethodError => e
19
- raise Error, "cannot find #{e.name} from #{node.to_commonmark.inspect} at #{node.sourcepos[:start_line]} in #{self} create block"
20
- rescue CommonMarker::NodeError
21
- raise Error, "could not get string content from #{node.to_commonmark.inspect} at #{node.sourcepos[:start_line]} in #{self} create block"
22
- end
10
+ begin
11
+ return collection_of_arguments_from_node(node).map { |args| _build(args, false) { |i| i.node = node } } if respond_to?(:collection_of_arguments_from_node)
12
+ rescue NoMethodError => e
13
+ raise Error, "cannot find #{e.name} from #{node.to_commonmark.inspect} at #{node.sourcepos[:start_line]} in #{self} collection_of_arguments_from_node"
14
+ rescue CommonMarker::NodeError
15
+ raise Error, "could not get string content from #{node.to_commonmark.inspect} at #{node.sourcepos[:start_line]} in #{self} collection_of_arguments_from_node"
16
+ end
23
17
 
24
- _build(args) { |i| i.node = node }
25
- else
26
- new.tap { |i| i.node = node }
18
+ begin
19
+ return _build(arguments_from_node(node)) { |i| i.node = node } if respond_to?(:arguments_from_node)
20
+ rescue NoMethodError => e
21
+ raise Error, "cannot find #{e.name} from #{node.to_commonmark.inspect} at #{node.sourcepos[:start_line]} in #{self} arguments_from_node"
22
+ rescue CommonMarker::NodeError
23
+ raise Error, "could not get string content from #{node.to_commonmark.inspect} at #{node.sourcepos[:start_line]} in #{self} arguments_from_node"
27
24
  end
25
+
26
+ new.tap { |i| i.node = node }
28
27
  end
29
28
 
30
29
  private def _build(args, recurse = true, &block)
@@ -32,20 +31,14 @@ module Grokdown
32
31
  when Hash
33
32
  if self < Hash
34
33
  new.merge!(args).tap(&block)
35
-
36
34
  else
37
35
  new(**args).tap(&block)
38
-
39
36
  end
40
37
  when Array
41
- if @create_many && recurse
42
- args.map { |i| _build(i, false, &block) }
43
- elsif self < Array
38
+ if self < Array
44
39
  new(args).tap(&block)
45
-
46
40
  else
47
41
  new(*args).tap(&block)
48
-
49
42
  end
50
43
  else
51
44
  new(*args).tap(&block)
@@ -1,7 +1,7 @@
1
1
  require "commonmarker"
2
2
  require "grokdown"
3
3
  require "grokdown/matching"
4
- require "grokdown/never_consumes"
4
+ require "grokdown/never_composes"
5
5
 
6
6
  module Grokdown
7
7
  class Document
@@ -14,7 +14,7 @@ module Grokdown
14
14
  when Matching
15
15
  Matching.for(node).build(node)
16
16
  else
17
- NeverConsumes.new(node)
17
+ NeverComposes.new(node)
18
18
  end
19
19
 
20
20
  doc.push decorated_node
@@ -37,8 +37,8 @@ module Grokdown
37
37
  end
38
38
 
39
39
  private def _push(node)
40
- if (accepts = @walk.reverse.find { |i| i.consumes?(node) })
41
- accepts.consume(node)
40
+ if (accepts = @walk.reverse.find { |i| i.can_compose?(node) })
41
+ accepts.add_composable(node)
42
42
  else
43
43
  @nodes.push(node)
44
44
  end
@@ -21,12 +21,10 @@ module Grokdown
21
21
  alias_method :===, :matches?
22
22
  end
23
23
 
24
- def match(&block)
25
- @matcher = block
26
- end
27
-
28
24
  def matches?(node)
29
- node.is_a?(self) || (node.is_a?(CommonMarker::Node) && @matcher.call(node))
25
+ node.is_a?(self) || (node.is_a?(CommonMarker::Node) && matches_node?(node))
26
+ rescue NoMethodError => e
27
+ raise NotImplementedError, "expected #{self} to implement #matches_node?(node) but got: #{e.message}"
30
28
  end
31
29
 
32
30
  alias_method :===, :matches?
@@ -1,8 +1,8 @@
1
1
  require "delegate"
2
2
 
3
3
  module Grokdown
4
- class NeverConsumes < SimpleDelegator
5
- def consumes?(*) = false
4
+ class NeverComposes < SimpleDelegator
5
+ def can_compose?(*) = false
6
6
 
7
7
  def ==(other)
8
8
  to_commonmark == other.to_commonmark
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grokdown
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/grokdown.rb CHANGED
@@ -1,8 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "grokdown/composing"
4
+ require_relative "grokdown/creating"
5
+ require_relative "grokdown/matching"
3
6
  require_relative "grokdown/version"
4
7
 
5
8
  module Grokdown
6
9
  class Error < StandardError; end
7
- # Your code goes here...
10
+
11
+ def self.included(base)
12
+ base.extend(Matching)
13
+ base.extend(Creating)
14
+ base.extend(Composing)
15
+ end
8
16
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grokdown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Caleb Buxton
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-17 00:00:00.000000000 Z
11
+ date: 2024-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: commonmarker
@@ -16,34 +16,37 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.20.1
19
+ version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.20.1
26
+ version: '0'
27
27
  description: Grok Markdown documents with Ruby objects.
28
28
  email:
29
29
  - me@cpb.ca
30
- executables: []
30
+ executables:
31
+ - grokdown
31
32
  extensions: []
32
33
  extra_rdoc_files: []
33
34
  files:
34
35
  - ".rspec"
35
36
  - ".ruby-version"
36
37
  - ".standard.yml"
38
+ - CHANGELOG.md
37
39
  - CODE_OF_CONDUCT.md
38
40
  - LICENSE.txt
39
41
  - README.md
40
42
  - Rakefile
43
+ - exe/grokdown
41
44
  - lib/grokdown.rb
42
- - lib/grokdown/consuming.rb
45
+ - lib/grokdown/composing.rb
43
46
  - lib/grokdown/creating.rb
44
47
  - lib/grokdown/document.rb
45
48
  - lib/grokdown/matching.rb
46
- - lib/grokdown/never_consumes.rb
49
+ - lib/grokdown/never_composes.rb
47
50
  - lib/grokdown/version.rb
48
51
  - sig/grokdown.rbs
49
52
  homepage: https://github.com/cpb/grokdown
@@ -52,7 +55,7 @@ licenses:
52
55
  metadata:
53
56
  homepage_uri: https://github.com/cpb/grokdown
54
57
  source_code_uri: https://github.com/cpb/grokdown
55
- changelog_uri: https://github.com/cpb/grokdown/blob/main/CODE_OF_CONDUCT.md
58
+ changelog_uri: https://github.com/cpb/grokdown/blob/main/CHANGELOG.md
56
59
  post_install_message:
57
60
  rdoc_options: []
58
61
  require_paths:
@@ -68,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
71
  - !ruby/object:Gem::Version
69
72
  version: '0'
70
73
  requirements: []
71
- rubygems_version: 3.5.9
74
+ rubygems_version: 3.5.14
72
75
  signing_key:
73
76
  specification_version: 4
74
77
  summary: Grok Markdown documents with Ruby objects.
@@ -1,35 +0,0 @@
1
- require "grokdown"
2
-
3
- module Grokdown
4
- module Consuming
5
- def self.extended(base)
6
- base.send(:include, InstanceMethods)
7
- end
8
-
9
- def consumes?(node)
10
- @consumables ||= {}
11
- @consumables.has_key?(node.class)
12
- end
13
-
14
- def consumes(mapping = {})
15
- @consumables = mapping
16
- end
17
-
18
- def consume(inst, node)
19
- @consumables ||= {}
20
- inst.send(@consumables.fetch(node.class), node)
21
- rescue KeyError
22
- raise ArgumentError, "#{inst.class} cannot consume #{node.class}"
23
- end
24
-
25
- module InstanceMethods
26
- def consumes?(node)
27
- self.class.consumes?(node)
28
- end
29
-
30
- def consume(node)
31
- self.class.consume(self, node)
32
- end
33
- end
34
- end
35
- end