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 +4 -4
- data/CHANGELOG.md +1 -0
- data/README.md +104 -9
- data/exe/grokdown +21 -0
- data/lib/grokdown/composing.rb +21 -0
- data/lib/grokdown/creating.rb +16 -23
- data/lib/grokdown/document.rb +4 -4
- data/lib/grokdown/matching.rb +3 -5
- data/lib/grokdown/{never_consumes.rb → never_composes.rb} +2 -2
- data/lib/grokdown/version.rb +1 -1
- data/lib/grokdown.rb +9 -1
- metadata +12 -9
- data/lib/grokdown/consuming.rb +0 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac8a98fe8b785069748b8188029679769d84f1a4b7b8cf92abe5168669e0f5dd
|
4
|
+
data.tar.gz: e447bf05e89b2726b9715984d1e74fbb756c00400173efda99da59cedcc53bb8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
3
|
+
`Grokdown` provides **an experimental interface** for building value objects and composing them into entities from a Markdown document tree.
|
4
4
|
|
5
|
-
##
|
5
|
+
## Usage
|
6
6
|
|
7
|
-
|
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
|
-
|
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
|
-
|
14
|
-
|
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
|
data/lib/grokdown/creating.rb
CHANGED
@@ -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
|
-
|
16
|
-
args =
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
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
|
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)
|
data/lib/grokdown/document.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require "commonmarker"
|
2
2
|
require "grokdown"
|
3
3
|
require "grokdown/matching"
|
4
|
-
require "grokdown/
|
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
|
-
|
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.
|
41
|
-
accepts.
|
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
|
data/lib/grokdown/matching.rb
CHANGED
@@ -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) &&
|
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?
|
data/lib/grokdown/version.rb
CHANGED
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
|
-
|
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.
|
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-
|
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
|
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
|
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/
|
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/
|
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/
|
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.
|
74
|
+
rubygems_version: 3.5.14
|
72
75
|
signing_key:
|
73
76
|
specification_version: 4
|
74
77
|
summary: Grok Markdown documents with Ruby objects.
|
data/lib/grokdown/consuming.rb
DELETED
@@ -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
|