varar-core 0.5.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 +7 -0
- data/lib/varar/core/ast.rb +47 -0
- data/lib/varar/core/canonical_json.rb +47 -0
- data/lib/varar/core/cell_diff.rb +105 -0
- data/lib/varar/core/conformance.rb +264 -0
- data/lib/varar/core/deep_freeze.rb +25 -0
- data/lib/varar/core/diagnostics.rb +48 -0
- data/lib/varar/core/doc_string_diff.rb +36 -0
- data/lib/varar/core/drift.rb +185 -0
- data/lib/varar/core/execute.rb +191 -0
- data/lib/varar/core/failure_anchor.rb +28 -0
- data/lib/varar/core/hash.rb +27 -0
- data/lib/varar/core/matcher.rb +132 -0
- data/lib/varar/core/param_diff.rb +52 -0
- data/lib/varar/core/parse.rb +18 -0
- data/lib/varar/core/plan.rb +286 -0
- data/lib/varar/core/registry.rb +74 -0
- data/lib/varar/core/scanner.rb +346 -0
- data/lib/varar/core/sentences.rb +114 -0
- data/lib/varar/core/span.rb +84 -0
- data/lib/varar/core/step_role.rb +23 -0
- data/lib/varar/core/structurer.rb +84 -0
- data/lib/varar/core/table_cells.rb +44 -0
- data/lib/varar/core.rb +32 -0
- metadata +79 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Varar
|
|
4
|
+
module Core
|
|
5
|
+
# Split a block of plain text into sentences on . ! ? and \n, skipping
|
|
6
|
+
# terminators inside backtick spans and double-quoted strings and treating
|
|
7
|
+
# common abbreviations as non-breaking. All offsets are UTF-16 code units
|
|
8
|
+
# into the block text. Port of sentences.ts.
|
|
9
|
+
Sentence = Data.define(:text, :start_offset, :end_offset)
|
|
10
|
+
|
|
11
|
+
module Sentences
|
|
12
|
+
ABBREVIATIONS = Set.new(['e.g.', 'i.e.', 'etc.', 'cf.', 'vs.']).freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def split_sentences(text)
|
|
17
|
+
cp_to_u16 = build_cp_to_u16(text)
|
|
18
|
+
n = text.length
|
|
19
|
+
out = []
|
|
20
|
+
|
|
21
|
+
# Mark no-split zones (backtick spans, double-quoted strings).
|
|
22
|
+
skip = Array.new(n, false)
|
|
23
|
+
j = 0
|
|
24
|
+
while j < n
|
|
25
|
+
c = text[j]
|
|
26
|
+
if ['`', '"'].include?(c)
|
|
27
|
+
close = text.index(c, j + 1)
|
|
28
|
+
break if close.nil?
|
|
29
|
+
|
|
30
|
+
(j..close).each { |k| skip[k] = true }
|
|
31
|
+
j = close + 1
|
|
32
|
+
next
|
|
33
|
+
end
|
|
34
|
+
j += 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
i = 0
|
|
38
|
+
segment_start = 0
|
|
39
|
+
while i < n
|
|
40
|
+
if skip[i]
|
|
41
|
+
i += 1
|
|
42
|
+
next
|
|
43
|
+
end
|
|
44
|
+
ch = text[i]
|
|
45
|
+
if ["\n", '.', '!', '?'].include?(ch)
|
|
46
|
+
if ch == '.' && inside_number_or_abbrev?(text, i)
|
|
47
|
+
i += 1
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
stop = i + 1
|
|
51
|
+
push_segment(out, text, segment_start, stop, cp_to_u16)
|
|
52
|
+
i = stop
|
|
53
|
+
i += 1 while i < n && [' ', "\n"].include?(text[i])
|
|
54
|
+
segment_start = i
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
i += 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
push_segment(out, text, segment_start, n, cp_to_u16)
|
|
61
|
+
out
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# cp_to_u16[cp_i] is the UTF-16 offset of text[cp_i].
|
|
65
|
+
def build_cp_to_u16(text)
|
|
66
|
+
result = Array.new(text.length + 1, 0)
|
|
67
|
+
u16 = 0
|
|
68
|
+
text.each_char.with_index do |ch, i|
|
|
69
|
+
result[i] = u16
|
|
70
|
+
u16 += ch.ord > 0xFFFF ? 2 : 1
|
|
71
|
+
end
|
|
72
|
+
result[text.length] = u16
|
|
73
|
+
result
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def inside_number_or_abbrev?(text, dot_pos)
|
|
77
|
+
prev = dot_pos.positive? ? text[dot_pos - 1] : ''
|
|
78
|
+
nxt = dot_pos + 1 < text.length ? text[dot_pos + 1] : ''
|
|
79
|
+
return true if digit?(prev) && digit?(nxt)
|
|
80
|
+
|
|
81
|
+
ABBREVIATIONS.each do |abbrev|
|
|
82
|
+
start = [0, dot_pos + 1 - abbrev.length].max
|
|
83
|
+
return true if text[start...(dot_pos + 1)] == abbrev
|
|
84
|
+
end
|
|
85
|
+
lower?(nxt)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def push_segment(out, text, start_cp, end_cp, cp_to_u16)
|
|
89
|
+
return if end_cp <= start_cp
|
|
90
|
+
|
|
91
|
+
raw = text[start_cp...end_cp]
|
|
92
|
+
stripped = raw.strip
|
|
93
|
+
return if stripped.empty?
|
|
94
|
+
|
|
95
|
+
lead = raw.length - raw.lstrip.length
|
|
96
|
+
trail = raw.length - raw.rstrip.length
|
|
97
|
+
out << Sentence.new(
|
|
98
|
+
text: stripped,
|
|
99
|
+
start_offset: cp_to_u16[start_cp + lead],
|
|
100
|
+
end_offset: cp_to_u16[end_cp - trail]
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def digit?(ch)
|
|
105
|
+
!ch.empty? && ch.match?(/[0-9]/)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Unicode-aware "is a lowercase letter": has case and is already lower.
|
|
109
|
+
def lower?(ch)
|
|
110
|
+
!ch.empty? && ch != ch.upcase && ch == ch.downcase
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Varar
|
|
4
|
+
module Core
|
|
5
|
+
# A source span. Offsets and columns are **UTF-16 code units** (an astral
|
|
6
|
+
# character like 😀 counts as 2), matching the goldens and LSP's default
|
|
7
|
+
# position encoding. Lines/cols are 1-based.
|
|
8
|
+
Span = Data.define(
|
|
9
|
+
:start_offset, :end_offset,
|
|
10
|
+
:start_line, :start_col,
|
|
11
|
+
:end_line, :end_col
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# UTF-16 offset conversion. Ruby strings are code-point indexed, so the
|
|
15
|
+
# whole core converts to/from UTF-16 code units here (the single riskiest
|
|
16
|
+
# part of the port — mirrors Python's span.py). Reused by the matcher and
|
|
17
|
+
# by hash.rb.
|
|
18
|
+
module Offsets
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# UTF-16 code-unit length of a string (astral chars count as 2).
|
|
22
|
+
def utf16_len(str)
|
|
23
|
+
n = 0
|
|
24
|
+
str.each_char { |ch| n += ch.ord > 0xFFFF ? 2 : 1 }
|
|
25
|
+
n
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# UTF-16 offset of the code-point index `cp_index` in `source`.
|
|
29
|
+
def to_utf16_offset(source, cp_index)
|
|
30
|
+
utf16_len(source[0...cp_index])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Inverse of to_utf16_offset: the code-point index at a UTF-16 offset.
|
|
34
|
+
def cp_index_for_utf16(source, u16)
|
|
35
|
+
count = 0
|
|
36
|
+
source.each_char.with_index do |ch, i|
|
|
37
|
+
return i if count >= u16
|
|
38
|
+
|
|
39
|
+
count += ch.ord > 0xFFFF ? 2 : 1
|
|
40
|
+
end
|
|
41
|
+
source.length
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Slice `source` by UTF-16 offsets, returning the covered substring.
|
|
45
|
+
def utf16_slice(source, start_u16, end_u16)
|
|
46
|
+
a = cp_index_for_utf16(source, start_u16)
|
|
47
|
+
b = cp_index_for_utf16(source, end_u16)
|
|
48
|
+
source[a...b]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 1-based [line, col] at a UTF-16 offset; col counts UTF-16 units and
|
|
52
|
+
# resets to 1 after each newline (mirrors span.ts's lineCol).
|
|
53
|
+
def line_col(source, offset_u16)
|
|
54
|
+
line = 1
|
|
55
|
+
col = 1
|
|
56
|
+
count = 0
|
|
57
|
+
source.each_char do |ch|
|
|
58
|
+
break if count >= offset_u16
|
|
59
|
+
|
|
60
|
+
width = ch.ord > 0xFFFF ? 2 : 1
|
|
61
|
+
if ch == "\n"
|
|
62
|
+
line += 1
|
|
63
|
+
col = 1
|
|
64
|
+
else
|
|
65
|
+
col += width
|
|
66
|
+
end
|
|
67
|
+
count += width
|
|
68
|
+
end
|
|
69
|
+
[line, col]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Build a Span from UTF-16 start/end offsets.
|
|
73
|
+
def span_from_offsets(source, start_u16, end_u16)
|
|
74
|
+
start_line, start_col = line_col(source, start_u16)
|
|
75
|
+
end_line, end_col = line_col(source, end_u16)
|
|
76
|
+
Span.new(
|
|
77
|
+
start_offset: start_u16, end_offset: end_u16,
|
|
78
|
+
start_line: start_line, start_col: start_col,
|
|
79
|
+
end_line: end_line, end_col: end_col
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Varar
|
|
4
|
+
module Core
|
|
5
|
+
# The role a step definition plays:
|
|
6
|
+
# "stimulus" — drives the software (arranges and acts on state)
|
|
7
|
+
# "sensor" — the read-only assertion (the only role that returns for
|
|
8
|
+
# comparison)
|
|
9
|
+
# Purely structural — never inspects sentence words (no Given/When/Then
|
|
10
|
+
# heuristics). Port of step-role.ts.
|
|
11
|
+
module StepRole
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Guess a step's role from its document-order neighbours. A step with
|
|
15
|
+
# nothing after it is most likely the observation; anything followed by
|
|
16
|
+
# other steps is most likely driving the software.
|
|
17
|
+
def infer_step_role(neighbours)
|
|
18
|
+
after = neighbours[:after] || neighbours['after'] || []
|
|
19
|
+
after.empty? ? 'sensor' : 'stimulus'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'varar/core/span'
|
|
4
|
+
require 'varar/core/ast'
|
|
5
|
+
|
|
6
|
+
module Varar
|
|
7
|
+
module Core
|
|
8
|
+
# Group scanned blocks into Examples, tracking heading scope and orphan
|
|
9
|
+
# attachments. Port of structurer.ts.
|
|
10
|
+
module Structurer
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def structure(path, source, blocks)
|
|
14
|
+
examples = []
|
|
15
|
+
orphan_attachments = []
|
|
16
|
+
scope_stack = [] # [[level, text], ...]
|
|
17
|
+
last_example_idx = -1
|
|
18
|
+
attachment_open = false
|
|
19
|
+
|
|
20
|
+
blocks.each do |block|
|
|
21
|
+
case block.kind
|
|
22
|
+
when 'heading'
|
|
23
|
+
# Pop deeper-or-equal-level entries before pushing the new heading.
|
|
24
|
+
scope_stack.pop while !scope_stack.empty? && scope_stack.last[0] >= block.level
|
|
25
|
+
scope_stack << [block.level, block.text]
|
|
26
|
+
attachment_open = false
|
|
27
|
+
|
|
28
|
+
when 'paragraph', 'list_item', 'blockquote'
|
|
29
|
+
# Merge a block into the previous example when that example's last
|
|
30
|
+
# block is an attachment (table/fence) with no blank line between.
|
|
31
|
+
if attachment_open && last_example_idx >= 0
|
|
32
|
+
prev = examples[last_example_idx]
|
|
33
|
+
prev_last = prev.body.last
|
|
34
|
+
last_is_attachment = !prev_last.nil? && %w[table fence].include?(prev_last.kind)
|
|
35
|
+
if last_is_attachment
|
|
36
|
+
between = Offsets.utf16_slice(source, prev.span.end_offset, block.span.start_offset)
|
|
37
|
+
unless between.match?(/\n\s*\n/)
|
|
38
|
+
new_span = Offsets.span_from_offsets(source, prev.span.start_offset, block.span.end_offset)
|
|
39
|
+
examples[last_example_idx] = Example.new(
|
|
40
|
+
scope_stack: prev.scope_stack,
|
|
41
|
+
span: new_span,
|
|
42
|
+
body: prev.body + [block]
|
|
43
|
+
)
|
|
44
|
+
next
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
examples << Example.new(
|
|
50
|
+
scope_stack: scope_stack.map { |(_, text)| text },
|
|
51
|
+
span: block.span,
|
|
52
|
+
body: [block]
|
|
53
|
+
)
|
|
54
|
+
last_example_idx = examples.length - 1
|
|
55
|
+
attachment_open = true
|
|
56
|
+
|
|
57
|
+
when 'table', 'fence'
|
|
58
|
+
if attachment_open && last_example_idx >= 0
|
|
59
|
+
prev = examples[last_example_idx]
|
|
60
|
+
new_span = Offsets.span_from_offsets(source, prev.span.start_offset, block.span.end_offset)
|
|
61
|
+
examples[last_example_idx] = Example.new(
|
|
62
|
+
scope_stack: prev.scope_stack,
|
|
63
|
+
span: new_span,
|
|
64
|
+
body: prev.body + [block]
|
|
65
|
+
)
|
|
66
|
+
else
|
|
67
|
+
orphan_attachments << block
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
when 'thematic_break'
|
|
71
|
+
attachment_open = false
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
VarDoc.new(
|
|
76
|
+
path: path,
|
|
77
|
+
source: source,
|
|
78
|
+
examples: examples,
|
|
79
|
+
orphan_attachments: orphan_attachments
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'varar/core/span'
|
|
4
|
+
|
|
5
|
+
module Varar
|
|
6
|
+
module Core
|
|
7
|
+
# Parse a Markdown/Gherkin table row into trimmed cells and per-cell source
|
|
8
|
+
# spans. Port of table-cells.ts. All offsets count UTF-16 code units.
|
|
9
|
+
module TableCells
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Split a `| a | b |` row into [cells, cell_spans]. +line_start_offset+
|
|
13
|
+
# is the UTF-16 offset of the row's first character within +source+.
|
|
14
|
+
def parse_row_cells(line_text, line_start_offset, source)
|
|
15
|
+
first_cp = line_text.index('|')
|
|
16
|
+
last_cp = line_text.rindex('|')
|
|
17
|
+
return [[], []] if first_cp.nil? || last_cp.nil? || last_cp <= first_cp
|
|
18
|
+
|
|
19
|
+
# Pipe positions: convert code-point index to UTF-16 (matters when
|
|
20
|
+
# astral chars precede the pipe). '|' is ASCII → 1 UTF-16 unit.
|
|
21
|
+
first_u16 = Offsets.to_utf16_offset(line_text, first_cp)
|
|
22
|
+
inner_start_u16 = first_u16 + 1
|
|
23
|
+
|
|
24
|
+
inner = line_text[(first_cp + 1)...last_cp]
|
|
25
|
+
|
|
26
|
+
cells = []
|
|
27
|
+
cell_spans = []
|
|
28
|
+
cursor = 0 # running UTF-16 position within inner
|
|
29
|
+
|
|
30
|
+
# split(-1) keeps trailing empty segments, matching JS/Python split.
|
|
31
|
+
inner.split('|', -1).each do |seg|
|
|
32
|
+
trimmed = seg.strip
|
|
33
|
+
leading = Offsets.utf16_len(seg) - Offsets.utf16_len(seg.lstrip)
|
|
34
|
+
abs_start = line_start_offset + inner_start_u16 + cursor + leading
|
|
35
|
+
cells << trimmed
|
|
36
|
+
cell_spans << Offsets.span_from_offsets(source, abs_start, abs_start + Offsets.utf16_len(trimmed))
|
|
37
|
+
cursor += Offsets.utf16_len(seg) + 1 # +1 for the '|' delimiter
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
[cells, cell_spans]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/varar/core.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Varar
|
|
4
|
+
# The pure functional core: parse, match, plan, execute, diffs, drift, and
|
|
5
|
+
# the conformance projections. No filesystem, network, globals, or time.
|
|
6
|
+
module Core
|
|
7
|
+
VERSION = '0.5.0'
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require 'varar/core/span'
|
|
12
|
+
require 'varar/core/ast'
|
|
13
|
+
require 'varar/core/table_cells'
|
|
14
|
+
require 'varar/core/scanner'
|
|
15
|
+
require 'varar/core/structurer'
|
|
16
|
+
require 'varar/core/parse'
|
|
17
|
+
require 'varar/core/step_role'
|
|
18
|
+
require 'varar/core/registry'
|
|
19
|
+
require 'varar/core/sentences'
|
|
20
|
+
require 'varar/core/diagnostics'
|
|
21
|
+
require 'varar/core/cell_diff'
|
|
22
|
+
require 'varar/core/matcher'
|
|
23
|
+
require 'varar/core/plan'
|
|
24
|
+
require 'varar/core/deep_freeze'
|
|
25
|
+
require 'varar/core/doc_string_diff'
|
|
26
|
+
require 'varar/core/param_diff'
|
|
27
|
+
require 'varar/core/failure_anchor'
|
|
28
|
+
require 'varar/core/execute'
|
|
29
|
+
require 'varar/core/hash'
|
|
30
|
+
require 'varar/core/drift'
|
|
31
|
+
require 'varar/core/canonical_json'
|
|
32
|
+
require 'varar/core/conformance'
|
metadata
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: varar-core
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.5.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Aslak Hellesøy
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: cucumber-cucumber-expressions
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - '='
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 20.0.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - '='
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 20.0.0
|
|
26
|
+
description: The pure functional pipeline (parse, match, plan, execute, drift) behind
|
|
27
|
+
Vár.
|
|
28
|
+
email:
|
|
29
|
+
- aslak@oselvar.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- lib/varar/core.rb
|
|
35
|
+
- lib/varar/core/ast.rb
|
|
36
|
+
- lib/varar/core/canonical_json.rb
|
|
37
|
+
- lib/varar/core/cell_diff.rb
|
|
38
|
+
- lib/varar/core/conformance.rb
|
|
39
|
+
- lib/varar/core/deep_freeze.rb
|
|
40
|
+
- lib/varar/core/diagnostics.rb
|
|
41
|
+
- lib/varar/core/doc_string_diff.rb
|
|
42
|
+
- lib/varar/core/drift.rb
|
|
43
|
+
- lib/varar/core/execute.rb
|
|
44
|
+
- lib/varar/core/failure_anchor.rb
|
|
45
|
+
- lib/varar/core/hash.rb
|
|
46
|
+
- lib/varar/core/matcher.rb
|
|
47
|
+
- lib/varar/core/param_diff.rb
|
|
48
|
+
- lib/varar/core/parse.rb
|
|
49
|
+
- lib/varar/core/plan.rb
|
|
50
|
+
- lib/varar/core/registry.rb
|
|
51
|
+
- lib/varar/core/scanner.rb
|
|
52
|
+
- lib/varar/core/sentences.rb
|
|
53
|
+
- lib/varar/core/span.rb
|
|
54
|
+
- lib/varar/core/step_role.rb
|
|
55
|
+
- lib/varar/core/structurer.rb
|
|
56
|
+
- lib/varar/core/table_cells.rb
|
|
57
|
+
homepage: https://varar.dev
|
|
58
|
+
licenses:
|
|
59
|
+
- MIT
|
|
60
|
+
metadata:
|
|
61
|
+
rubygems_mfa_required: 'true'
|
|
62
|
+
rdoc_options: []
|
|
63
|
+
require_paths:
|
|
64
|
+
- lib
|
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '3.2'
|
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
requirements: []
|
|
76
|
+
rubygems_version: 4.0.16
|
|
77
|
+
specification_version: 4
|
|
78
|
+
summary: Markdown-native BDD — pure functional core engine
|
|
79
|
+
test_files: []
|