grokdown 0.3.0 → 0.4.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.
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