lite-utils-md_code 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d4f6dae34cdd94c22ab7eec0c7f639b844a5c665c3ceee865117609a65024602
4
+ data.tar.gz: 0350753f46a05fa6ac07d0c5a0a4d29bbfccc13fe0be3d282e571536f5560c29
5
+ SHA512:
6
+ metadata.gz: d5680075287f717edd30aac0e2ea468d40269330ccd9d0e914d9c720a80ec6dd10f52438b08e2ca2f72cdf7a75e23c92381869fbe386a298a50e430e571d31d5
7
+ data.tar.gz: 3f9fa8897dd8e3ff95985b83979cbb3785eca986237bc75a8da563237565e12786ac4f269056cd58a10b7d55c7b38aad8d87b19f5ca08d37852297cbf293220a
data/.rubocop.yml ADDED
@@ -0,0 +1,88 @@
1
+ plugins:
2
+ - rubocop-rspec
3
+
4
+ AllCops:
5
+ NewCops: enable
6
+
7
+ Layout/EndAlignment:
8
+ EnforcedStyleAlignWith: start_of_line
9
+
10
+ Layout/LineLength:
11
+ Exclude:
12
+ - spec/**/*.rb
13
+
14
+ Lint/BooleanSymbol:
15
+ Enabled: false
16
+
17
+ Lint/ConstantDefinitionInBlock:
18
+ Exclude:
19
+ - spec/**/*.rb
20
+
21
+ Lint/ShadowingOuterLocalVariable:
22
+ Enabled: false
23
+
24
+ Metrics/BlockLength:
25
+ Exclude:
26
+ - spec/**/*.rb
27
+
28
+ Metrics/MethodLength:
29
+ Max: 20
30
+
31
+ Metrics/ModuleLength:
32
+ Max: 200
33
+
34
+ Naming/BinaryOperatorParameterName:
35
+ Enabled: false
36
+
37
+ Naming/BlockForwarding:
38
+ Enabled: false
39
+
40
+ Naming/PredicateMethod:
41
+ Enabled: false
42
+
43
+ Naming/VariableNumber:
44
+ Exclude:
45
+ - spec/**/*.rb
46
+
47
+ RSpec/ExampleLength:
48
+ Max: 10
49
+
50
+ RSpec/IndexedLet:
51
+ Enabled: false
52
+
53
+ RSpec/MultipleMemoizedHelpers:
54
+ Enabled: false
55
+
56
+ Style/ArgumentsForwarding:
57
+ Enabled: false
58
+
59
+ Style/Documentation:
60
+ Enabled: false
61
+
62
+ Style/DocumentDynamicEvalDefinition:
63
+ Enabled: false
64
+
65
+ Style/EachWithObject:
66
+ Enabled: false
67
+
68
+ Style/HashConversion:
69
+ Enabled: false
70
+
71
+ Style/HashSyntax:
72
+ Enabled: false
73
+
74
+ Style/MethodCallWithoutArgsParentheses:
75
+ Enabled: false
76
+
77
+ Style/MultilineBlockChain:
78
+ Enabled: false
79
+
80
+ Style/RedundantArrayConstructor:
81
+ Enabled: false
82
+
83
+ Style/RedundantSelfAssignment:
84
+ Enabled: false
85
+
86
+ Style/TrailingUnderscoreVariable:
87
+ Enabled: false
88
+
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :test do
8
+ gem 'byebug', '~> 11'
9
+ gem 'rspec', '~> 3'
10
+ gem 'rubocop'
11
+ gem 'rubocop-rspec'
12
+ gem 'simplecov'
13
+ end
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # lite-utils-md_code
2
+ Extracts runnable code inside MD fences,
3
+ tagged with an identifier, so you can `eval`
4
+ them from your test suite. The main use case:
5
+ keeping documentation examples honest by actually
6
+ running them as specs.
7
+
8
+ ## Writing the documentation
9
+ Write runnable code in the MD file and mark it
10
+ with a tag and a unique identifier. The
11
+ fenced block looks somewhat like this in raw MD:
12
+ ````ruby test rspec_example
13
+ ```ruby test rspec_example
14
+ expect(true).to be(true)
15
+ ```
16
+ ````
17
+
18
+ The info string after the opening triple-backtick follows a fixed
19
+ grammar: `<language> <tag> <key>`, space-separated.
20
+
21
+ - `language` – the language identifier used for syntax highlighting
22
+ (e.g. `ruby`). Not interpreted by this library.
23
+ - `tag` – groups snippets so they can be filtered together. Matches
24
+ the `tag:` keyword passed to `MdCode.instance`.
25
+ - `key` – an identifier for this specific snippet, used later
26
+ to reference it via `SPECS.snippet!(:key)`. Must be unique
27
+ in a tag.
28
+
29
+ So ` ```ruby test unique_identifier ` declares a Ruby snippet, tagged
30
+ `test`, retrievable under the key `unique_identifier`.
31
+
32
+ ## Writing the tests
33
+ Load the snippets somewhere in your specs. The positional
34
+ argument is the path to the file containing code snippets,
35
+ optional `tag` keyword specifies the tag used to filter
36
+ them – defaults to `:test`. Another
37
+ optional keyword is `dir`, which can be used to pass over
38
+ `__dir__` from the caller's location.
39
+ ```ruby test extract_snippets
40
+ SPECS = MdCode.instance('../../../README.md', dir: __dir__)
41
+ ```
42
+
43
+ Configure RSpec so that it ensures all snippets have been consumed
44
+ ```ruby test rspec_config
45
+ RSpec.configure do |config|
46
+ config.add_setting :readme_spec, default: false
47
+
48
+ config.define_derived_metadata(file_path: %r{spec/utils/md_code}) do |meta|
49
+ config.readme_spec = true
50
+ end
51
+
52
+ config.after(:suite) do
53
+ next unless config.readme_spec
54
+
55
+ SPECS.ensure_consumed!
56
+ end
57
+ end
58
+ ```
59
+
60
+ You may need a more complex configuration depending on
61
+ how you structure your specs; however, the setup shown
62
+ above works even with complex readme specs spreading
63
+ across multiple files.
64
+
65
+ Eval the snippets inside your specs – either as a part of the context
66
+ or inside your examples. Note the bang at the end of the method name –
67
+ this method tracks usage of individual keys and raises if it was
68
+ called twice with the same key. This makes it easier to avoid mistakes
69
+ where your test evals some key by mistake and omits another,
70
+ making sure your whole documentation is tested as intended.
71
+ ```ruby test rspec_eval
72
+ it 'is true' do
73
+ eval(SPECS.snippet!(:rspec_example))
74
+ end
75
+ ```
76
+
77
+ Obviously the bang variant is meant for snippets that run
78
+ exactly once across the whole suite. When the snippet
79
+ represents shared setup – like a memoized helper body –
80
+ it must be allowed to evaluate repeatedly. In such
81
+ case use `SPECS.snippet` without the bang. It still tracks
82
+ usage but doesn't raise when the key is accessed multiple times.
83
+
84
+ **A note on `eval`**: This library works by evaluating snippet contents
85
+ as Ruby source. That's inherent to the pattern – the whole point is
86
+ to run your documentation's example code.
87
+
88
+ Snippets only ever come from files that already live in your own
89
+ repository, and they're only evaluated inside your own test suite.
90
+ You're not pulling snippets from user input, a network request,
91
+ or any other untrusted source – just from docs you wrote yourself.
92
+
93
+ ## License
94
+ This library is published under the MIT license
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Utils
5
+ module MdCode
6
+ class Error < StandardError; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'markly'
4
+
5
+ require_relative 'error'
6
+
7
+ module Lite
8
+ module Utils
9
+ module MdCode
10
+ module Extraction
11
+ def self.extract(path, dir, tag)
12
+ extract_each(Markly.parse(read(path, dir)), tag)
13
+ end
14
+
15
+ def self.extract_each(doc, tag)
16
+ specs = {}
17
+ regex = Regexp.new("^\\S+\\s+#{tag}\\s+(\\S+)$")
18
+
19
+ doc.walk do |node|
20
+ next unless node.type == :code_block
21
+ next unless (match = regex.match(node.fence_info))
22
+
23
+ key = match[1].to_sym
24
+ raise Error, "Duplicate #{tag} key: #{key}" if specs.key?(key)
25
+
26
+ specs.store(key, node.string_content.freeze)
27
+ end
28
+
29
+ specs.freeze
30
+ end
31
+
32
+ def self.read(path, dir)
33
+ full_path(path, dir).read
34
+ end
35
+
36
+ def self.full_path(path, dir)
37
+ dir ? Pathname.new(dir).join(path) : Pathname.new(path)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lite/data'
4
+
5
+ require_relative 'error'
6
+
7
+ module Lite
8
+ module Utils
9
+ module MdCode
10
+ class Instance
11
+ Lite::Data.define(self, args: %i[snippets usage])
12
+
13
+ def self.instance(snippets)
14
+ new snippets, {}
15
+ end
16
+
17
+ def initialize(snippets, usage)
18
+ super(snippets.freeze, usage)
19
+ end
20
+
21
+ def snippet!(key)
22
+ consume key, true
23
+ end
24
+
25
+ def snippet(key)
26
+ consume key, false
27
+ end
28
+
29
+ def consume(key, deplete)
30
+ raise Error, "Snippet not defined: #{key}" unless snippets.key?(key)
31
+ raise Error, "Key already used: #{key}" if usage[key] == :depleted
32
+
33
+ case usage[key]
34
+ when nil, :consumed then usage[key] = deplete ? :depleted : :consumed
35
+ end
36
+ snippets[key]
37
+ end
38
+
39
+ def ensure_consumed!
40
+ unconsumed = snippets.keys - usage.keys
41
+
42
+ raise Error, "Some snippets haven't been consumed: #{unconsumed.join(', ')}" unless unconsumed.empty?
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Utils
5
+ module MdCode
6
+ VERSION = '0.0.1'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'md_code/version'
4
+ require_relative 'md_code/extraction'
5
+ require_relative 'md_code/instance'
6
+
7
+ module Lite
8
+ module Utils
9
+ module MdCode
10
+ def self.instance(path, dir: nil, tag: :test)
11
+ snippets = Extraction.extract(path, dir, tag)
12
+ Instance.instance snippets
13
+ end
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lite-utils-md_code
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Tomas Milsimer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: lite-data
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: markly
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: |
42
+ Extracts runnable code inside MD fences, tagged with an identifier,
43
+ so you can `eval` them from your test suite.
44
+ email:
45
+ - tomas.milsimer@protonmail.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".rubocop.yml"
51
+ - Gemfile
52
+ - README.md
53
+ - lib/lite/utils/md_code.rb
54
+ - lib/lite/utils/md_code/error.rb
55
+ - lib/lite/utils/md_code/extraction.rb
56
+ - lib/lite/utils/md_code/instance.rb
57
+ - lib/lite/utils/md_code/version.rb
58
+ homepage: https://github.com/lame-impala/lite-utils-md_code
59
+ licenses:
60
+ - MIT
61
+ metadata:
62
+ rubygems_mfa_required: 'true'
63
+ allowed_push_host: https://rubygems.org
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 3.0.0
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.5.22
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Connects a MD file to your code through MD fencing
83
+ test_files: []