raindown 0.2.2 → 0.3.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/lib/raindown/chart/bars.rb +69 -0
- data/lib/raindown/chart/chart.rb +55 -0
- data/lib/raindown/chart/columns.rb +52 -0
- data/lib/raindown/chart/drawing.rb +88 -0
- data/lib/raindown/chart/line.rb +130 -0
- data/lib/raindown/chart/points.rb +140 -0
- data/lib/raindown/chart/sparkline.rb +27 -0
- data/lib/raindown/cli.rb +1 -1
- data/lib/raindown/config.rb +7 -1
- data/lib/raindown/node_blocks.rb +17 -6
- data/lib/raindown/nodes.rb +6 -5
- data/lib/raindown/page.rb +2 -1
- data/lib/raindown/pages.rb +2 -0
- data/lib/raindown/variables.rb +19 -0
- data/lib/raindown.rb +38 -12
- data/lib/version.rb +1 -1
- metadata +25 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee322a9f4df788bddfb51b6d669a7a676574620ecd1ce30e69a5682d49b8c88e
|
|
4
|
+
data.tar.gz: 2bb5f698d9c5e90f0c44271e4b1529512dab858dcd07beb04061ac8690a81bff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e550608449c87c693edef3c249472a89c62618de92941b81e135250462bbc74a062b61b2e025ea8666ff571b5057a25b67a13ed6fb385f304e500448f09a3034
|
|
7
|
+
data.tar.gz: a60db4e479d593ea779e93c3c683401519599ac0bf1d07f6f1e2a280634eff26b5f46167f27d5ce24fcd08f54660cb4051ed21e0c422951247a8f3d2c53938a7
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'drawing'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
class Chart < Node
|
|
7
|
+
# Horizontal bars, with negative values or a minus column left of a line:
|
|
8
|
+
# Jan ████████████▍ 1,240 a1b2c3d -38 ▕████│███████████▍ +142
|
|
9
|
+
class Bars < Drawing
|
|
10
|
+
LEFT = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉'].freeze
|
|
11
|
+
# Negative bars fill a cell from the right, which only has eighths and halves.
|
|
12
|
+
RIGHT = ['', '▕', '▕', '▐', '▐', '▐', FULL, FULL].freeze
|
|
13
|
+
|
|
14
|
+
def lines
|
|
15
|
+
measure
|
|
16
|
+
values = value_texts
|
|
17
|
+
minuses = minus_texts
|
|
18
|
+
@labels.each_index.map { |index| "#{label_text(@labels[index])}#{minuses[index]}#{bar_line(index)} #{values[index]}" }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# How many characters a value takes, and how many the bars take either side of the line.
|
|
24
|
+
def measure
|
|
25
|
+
low, high = range
|
|
26
|
+
@scale = @width / (high - low).to_f
|
|
27
|
+
@left = cells(-low * @scale)
|
|
28
|
+
@right = cells(high * @scale)
|
|
29
|
+
@line = low.negative? ? '│' : ''
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Each minus value lined up left of its bar, or nothing without a minus column.
|
|
33
|
+
def minus_texts
|
|
34
|
+
return Array.new(@labels.length, '') unless @minus
|
|
35
|
+
|
|
36
|
+
aligned(@below.map { "-#{number_text(it)}" }).map { "#{it} " }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Each value, lined up, with a + when a minus column is charted against it, whose values go left of the bars.
|
|
40
|
+
def value_texts
|
|
41
|
+
aligned(@minus ? @above.map { "+#{number_text(it)}" } : @values.map { number_text(it) })
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def aligned(texts)
|
|
45
|
+
texts.map { it.rjust(texts.map(&:length).max) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def bar_line(index)
|
|
49
|
+
negative = @below[index].positive? ? bar(@below[index] * @scale, RIGHT, from: :right) : ''
|
|
50
|
+
positive = @above[index].positive? ? bar(@above[index] * @scale, LEFT) : ''
|
|
51
|
+
|
|
52
|
+
"#{negative.rjust(@left)}#{@line}#{pad(positive, @right)}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def label_text(label)
|
|
56
|
+
label_width = @labels.map { display_width(it) }.max
|
|
57
|
+
label_width.zero? ? '' : "#{pad(label, label_width)} "
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A bar of full cells and an eighth, where any value above zero shows.
|
|
61
|
+
def bar(length, partials, from: :left)
|
|
62
|
+
eighths = (length * 8).round.clamp(1..)
|
|
63
|
+
full, part = eighths.divmod(8)
|
|
64
|
+
|
|
65
|
+
from == :left ? (FULL * full) + partials[part] : partials[part] + (FULL * full)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../node'
|
|
4
|
+
require_relative 'bars'
|
|
5
|
+
require_relative 'columns'
|
|
6
|
+
require_relative 'line'
|
|
7
|
+
require_relative 'points'
|
|
8
|
+
require_relative 'sparkline'
|
|
9
|
+
|
|
10
|
+
module Raindown
|
|
11
|
+
# The node behind chart:, a chart in block characters of the data it's given, written into the Markdown as code:
|
|
12
|
+
# chart: 'data/downloads.csv' chart: stats format: 'columns' chart: '3 5 -2 8'
|
|
13
|
+
# The data is a CSV, TSV, JSON or YAML file relative to the project, numbers, or a variable with either, or with
|
|
14
|
+
# data of its own like a list or a hash. Labeled data draws as bars and numbers alone as a sparkline,
|
|
15
|
+
# unless format: is 'bars', 'columns', 'line' or 'sparkline'. Negative values go below a line, or left of it for bars.
|
|
16
|
+
# A project can draw charts its own way with a Chart node in .raindown, like "class Chart < Raindown::Chart".
|
|
17
|
+
class Chart < Node
|
|
18
|
+
FORMATS = { 'bars' => Bars, 'columns' => Columns, 'line' => Line, 'sparkline' => Sparkline }.freeze
|
|
19
|
+
|
|
20
|
+
# Label and value are the columns to chart, by name or index, and minus a column to chart against the values.
|
|
21
|
+
# Width is in characters, for bars and lines, and height in lines, for columns and lines.
|
|
22
|
+
def initialize(data = nil, format: nil, label: nil, value: nil, minus: nil, width: 40, height: 8) # rubocop:disable Metrics/ParameterLists -- each is a prop
|
|
23
|
+
@data = data
|
|
24
|
+
@format = format
|
|
25
|
+
@label = label
|
|
26
|
+
@value = value
|
|
27
|
+
@minus = minus
|
|
28
|
+
@width = width
|
|
29
|
+
@height = height
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The data is read every time, so the chart follows changes to its file.
|
|
33
|
+
def replace(metadata: NoMetadata)
|
|
34
|
+
points = points(metadata:)
|
|
35
|
+
return '' if points.empty?
|
|
36
|
+
|
|
37
|
+
"```\n#{draw(points).map(&:rstrip).join("\n")}\n```"
|
|
38
|
+
rescue Error => e
|
|
39
|
+
"Can't chart #{@data.inspect}: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Each value with its label, or nil for numbers alone.
|
|
43
|
+
def points(metadata: NoMetadata)
|
|
44
|
+
root = metadata.respond_to?(:root) ? metadata.root : Dir.pwd
|
|
45
|
+
Points.new(label: @label, value: @value, minus: @minus, root:).from(@data)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def draw(points)
|
|
49
|
+
format = @format || (points.any?(&:first) ? 'bars' : 'sparkline')
|
|
50
|
+
drawing = FORMATS[format] || raise(Error, "format: is #{FORMATS.keys.join(', ')}, not #{format.inspect}")
|
|
51
|
+
|
|
52
|
+
drawing.new(points, width: @width, height: @height).lines
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'drawing'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
class Chart < Node
|
|
7
|
+
# Vertical columns from a line at zero, with negative values or a minus column below it,
|
|
8
|
+
# the largest and smallest values on the left and labels below.
|
|
9
|
+
# 2,210 ┤ ▄▄▄
|
|
10
|
+
# │ ███ ███
|
|
11
|
+
# 0 ┼─────────
|
|
12
|
+
# Jan Feb
|
|
13
|
+
class Columns < Drawing
|
|
14
|
+
def lines
|
|
15
|
+
low, high = range
|
|
16
|
+
@slot = [3, *@labels.map { display_width(it) }].max
|
|
17
|
+
above, below = grid(@height / (high - low).to_f, slot: @slot, gap: 1)
|
|
18
|
+
top, bottom = ticks
|
|
19
|
+
|
|
20
|
+
[*axis(above, top, at: 0), baseline, *axis(below, bottom, at: below.length - 1), *label_line]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# The largest value at the top, and the smallest at the bottom when it's below zero.
|
|
26
|
+
def ticks
|
|
27
|
+
top = @above.max.positive? ? number_text(@above.max) : ''
|
|
28
|
+
bottom = @below.max.positive? ? number_text(-@below.max) : ''
|
|
29
|
+
@tick_width = [top, bottom, '0'].map(&:length).max
|
|
30
|
+
|
|
31
|
+
[top, bottom]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Rows after the axis, with the tick on the row at its index.
|
|
35
|
+
def axis(rows, tick, at:)
|
|
36
|
+
rows.each_with_index.map do |row, index|
|
|
37
|
+
index == at && !tick.empty? ? "#{tick.rjust(@tick_width)} ┤ #{row}" : "#{' ' * @tick_width} │ #{row}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def baseline
|
|
42
|
+
"#{'0'.rjust(@tick_width)} ┼#{'─' * ((@values.length * (@slot + 1)) + 1)}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def label_line
|
|
46
|
+
return [] if @labels.all?(&:empty?)
|
|
47
|
+
|
|
48
|
+
["#{' ' * (@tick_width + 3)}#{@labels.map { center(it, @slot) }.join(' ')}"]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'unicode/display_width'
|
|
4
|
+
|
|
5
|
+
require_relative '../node'
|
|
6
|
+
|
|
7
|
+
module Raindown
|
|
8
|
+
class Chart < Node
|
|
9
|
+
class Drawing
|
|
10
|
+
FULL = '█'
|
|
11
|
+
# Columns fill a cell from the bottom, and negative columns from the top, which only has eighths and halves.
|
|
12
|
+
LOWER = ['', '▁', '▂', '▃', '▄', '▅', '▆', '▇'].freeze
|
|
13
|
+
UPPER = ['', '▔', '▔', '▀', '▀', '▀', FULL, FULL].freeze
|
|
14
|
+
# Float error, so a value of exactly 3 cells doesn't reach into a 4th.
|
|
15
|
+
EPSILON = 1e-9
|
|
16
|
+
|
|
17
|
+
# Values go above zero or right of its line, and negative values below or left of it. With a minus column,
|
|
18
|
+
# each point is [label, value, minus], and its value goes above and its minus below, whatever their signs.
|
|
19
|
+
def initialize(points, width:, height:)
|
|
20
|
+
@labels = points.map { it.first.to_s }
|
|
21
|
+
@values = points.map { it[1] }
|
|
22
|
+
@minus = points.map { it[2] } if points.any? { it.length > 2 }
|
|
23
|
+
@above = @minus ? @values.map(&:abs) : @values.map { it.clamp(0..) }
|
|
24
|
+
@below = @minus ? @minus.map(&:abs) : @values.map { (-it).clamp(0..) }
|
|
25
|
+
@width = width
|
|
26
|
+
@height = height
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# How far below and above zero the chart goes, which always includes zero, so bars and columns start from it.
|
|
32
|
+
def range
|
|
33
|
+
low = -@below.max
|
|
34
|
+
high = @above.max
|
|
35
|
+
|
|
36
|
+
[low, low == high ? 1 : high]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The rows of columns above and below zero, top to bottom, each column in the middle of its slot.
|
|
40
|
+
def grid(scale, slot:, gap:)
|
|
41
|
+
low, high = range
|
|
42
|
+
|
|
43
|
+
[grid_rows(cells(high * scale).downto(1), @above, scale, LOWER, slot:, gap:),
|
|
44
|
+
grid_rows(1.upto(cells(-low * scale)), @below, scale, UPPER, slot:, gap:)]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Each row by how many cells it is from zero, for the values above it or below it.
|
|
48
|
+
def grid_rows(rows, values, scale, partials, slot:, gap:) # rubocop:disable Metrics/ParameterLists -- a row of the grid
|
|
49
|
+
rows.map do |row|
|
|
50
|
+
values.map { |value| center(cell((value * scale) - row + 1, partials) * [slot, 3].min, slot) }.join(' ' * gap)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# How much of a cell a column fills, by eighths, where any value above zero shows.
|
|
55
|
+
def cell(level, partials)
|
|
56
|
+
return ' ' if level <= EPSILON
|
|
57
|
+
|
|
58
|
+
eighths = (level.clamp(..1) * 8).round.clamp(1..)
|
|
59
|
+
eighths == 8 ? FULL : partials[eighths]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def cells(length)
|
|
63
|
+
(length - EPSILON).ceil.clamp(0..)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Whole numbers with thousands separators, and others to two decimal places.
|
|
67
|
+
def number_text(value)
|
|
68
|
+
value = value.round(2)
|
|
69
|
+
value = value.to_i if value.is_a?(Float) && value == value.to_i
|
|
70
|
+
whole, decimal = value.abs.to_s.split('.')
|
|
71
|
+
|
|
72
|
+
"#{'-' if value.negative?}#{whole.reverse.scan(/\d{1,3}/).join(',').reverse}#{".#{decimal}" if decimal}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def display_width(text)
|
|
76
|
+
Unicode::DisplayWidth.of(text)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def pad(text, width)
|
|
80
|
+
text + (' ' * (width - display_width(text)))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def center(text, width)
|
|
84
|
+
pad((' ' * ((width - display_width(text)) / 2)) + text, width)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'drawing'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
class Chart < Node
|
|
7
|
+
# A line through the values in braille, where each character is a grid of dots 2 wide and 4 high,
|
|
8
|
+
# with the highest and lowest values on the left, labels below and negative values below a line at zero.
|
|
9
|
+
# With a minus column, its values are a second line below zero.
|
|
10
|
+
# 4 ┤ ⠑⡄ ⢀⠎
|
|
11
|
+
# 0 ┼──⠈⠢⡀──⡰⠁─
|
|
12
|
+
# -4 ┤ ⠘⢄⠎
|
|
13
|
+
# Jan Mar
|
|
14
|
+
class Line < Drawing
|
|
15
|
+
BRAILLE = 0x2800
|
|
16
|
+
# The bit of each dot in a braille character, by its row and then its column.
|
|
17
|
+
DOTS = [[0x01, 0x08], [0x02, 0x10], [0x04, 0x20], [0x40, 0x80]].freeze
|
|
18
|
+
|
|
19
|
+
def lines
|
|
20
|
+
@series = @minus ? [@above, @below.map(&:-@)] : [@values]
|
|
21
|
+
@low, @high = bounds
|
|
22
|
+
@cells = Array.new(@height) { Array.new(@width, 0) }
|
|
23
|
+
@series.each { plot_series(it) }
|
|
24
|
+
|
|
25
|
+
[*axis, *label_line]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def plot_series(values)
|
|
31
|
+
dots = values.each_index.map { |index| [x(index), y(values[index])] }
|
|
32
|
+
dots.each_cons(2) { |from, to| plot_line(*from, *to) }
|
|
33
|
+
plot(*dots.first) if dots.one?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The lowest and highest values, which include zero when there are negative values, so they go below its line.
|
|
37
|
+
def bounds
|
|
38
|
+
low, high = @series.flatten.minmax
|
|
39
|
+
high = [high, 0].max if low.negative?
|
|
40
|
+
|
|
41
|
+
[low, high]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def x(index)
|
|
45
|
+
return 0 if @values.one?
|
|
46
|
+
|
|
47
|
+
(index * ((@width * 2) - 1).to_f / (@values.length - 1)).round
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def y(value)
|
|
51
|
+
return (@height * 4) - 1 if @high == @low
|
|
52
|
+
|
|
53
|
+
((@high - value) * ((@height * 4) - 1).to_f / (@high - @low)).round
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The dots between two dots, a step at a time along whichever of across or down is longer.
|
|
57
|
+
def plot_line(x_from, y_from, x_to, y_to)
|
|
58
|
+
steps = [(x_to - x_from).abs, (y_to - y_from).abs, 1].max
|
|
59
|
+
0.upto(steps) { |step| plot(between(x_from, x_to, step, steps), between(y_from, y_to, step, steps)) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def between(from, to, step, steps)
|
|
63
|
+
from + ((to - from) * step.to_f / steps).round
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def plot(x, y) # rubocop:disable Naming/MethodParameterName -- a dot's coordinates
|
|
67
|
+
@cells[y / 4][x / 2] |= DOTS[y % 4][x % 2]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Each row of braille after the axis, with the highest value at the top, the lowest at the bottom,
|
|
71
|
+
# and zero on the line through the negative values' row.
|
|
72
|
+
def axis
|
|
73
|
+
@ticks = ticks
|
|
74
|
+
@tick_width = @ticks.values.map(&:length).max
|
|
75
|
+
|
|
76
|
+
@cells.each_with_index.map do |cells, row|
|
|
77
|
+
"#{@ticks[row].to_s.rjust(@tick_width)} #{mark(row)}#{cells.map { cell(it, row) }.join}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The values on the axis by their row, where the highest wins a row it shares with the lowest.
|
|
82
|
+
def ticks
|
|
83
|
+
ticks = { (@height - 1) => number_text(@low) }
|
|
84
|
+
ticks[0] = number_text(@high) unless @high == @low
|
|
85
|
+
ticks[zero_row] ||= '0' if zero_row
|
|
86
|
+
ticks
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def mark(row)
|
|
90
|
+
return '┼─' if row == zero_row
|
|
91
|
+
|
|
92
|
+
@ticks[row] ? '┤ ' : '│ '
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def zero_row
|
|
96
|
+
y(0) / 4 if @low.negative?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def cell(bits, row)
|
|
100
|
+
return (BRAILLE + bits).chr(Encoding::UTF_8) unless bits.zero?
|
|
101
|
+
|
|
102
|
+
row == zero_row ? '─' : ' '
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The labels under their values, the first and last ones first, leaving out any that would touch another.
|
|
106
|
+
def label_line
|
|
107
|
+
return [] if @labels.all?(&:empty?)
|
|
108
|
+
|
|
109
|
+
line = placed_labels.sort.reduce(+'') { |text, (from, _, label)| text << (' ' * (from - display_width(text))) << label }
|
|
110
|
+
["#{' ' * (@tick_width + 3)}#{line}"]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Each label that fits, with where it starts and ends.
|
|
114
|
+
def placed_labels
|
|
115
|
+
[0, @labels.length - 1, *(1...(@labels.length - 1))].uniq.each_with_object([]) do |index, placed|
|
|
116
|
+
span = label_span(index)
|
|
117
|
+
placed << [*span, @labels[index]] if placed.none? { |from, to| span[0] <= to + 1 && span[1] >= from - 1 }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Where a label starts and ends, centered on its value's column and kept inside the chart.
|
|
122
|
+
def label_span(index)
|
|
123
|
+
width = display_width(@labels[index])
|
|
124
|
+
from = ((x(index) / 2) - (width / 2)).clamp(0, [@width - width, 0].max)
|
|
125
|
+
|
|
126
|
+
[from, from + width - 1]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'csv'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
|
|
7
|
+
require_relative '../node'
|
|
8
|
+
|
|
9
|
+
module Raindown
|
|
10
|
+
class Chart < Node
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
# A chart's data as points, each value with its label, or with nil for numbers alone.
|
|
14
|
+
# Label and value are the columns to chart, by name or index, which default to the first column
|
|
15
|
+
# and the first column of numbers after it. Rows without a number are left out.
|
|
16
|
+
# Minus is a column of numbers to chart against the values, like lines deleted against lines added,
|
|
17
|
+
# which makes each point [label, value, minus].
|
|
18
|
+
class Points
|
|
19
|
+
NUMBER = /\A[+-]?(?:\d{1,3}(?:,\d{3})+|\d+)?(?:\.\d+)?\z/
|
|
20
|
+
|
|
21
|
+
def initialize(label: nil, value: nil, minus: nil, root: Dir.pwd)
|
|
22
|
+
@label = label
|
|
23
|
+
@value = value
|
|
24
|
+
@minus = minus
|
|
25
|
+
@root = root
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def from(data)
|
|
29
|
+
case data
|
|
30
|
+
when String then from_string(data)
|
|
31
|
+
when Numeric then numbers([data])
|
|
32
|
+
when Hash then from_table(data.to_a, header: false)
|
|
33
|
+
when Array then from_list(data)
|
|
34
|
+
else raise Error, 'there is no data'
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# A file's path, relative to the root, or numbers like '3 5 -2 8' or '3, 5, -2, 8'.
|
|
41
|
+
def from_string(string)
|
|
42
|
+
numbers = string.strip.split(/[\s,]+/)
|
|
43
|
+
return numbers(numbers) if numbers.any? && numbers.all? { number(it) }
|
|
44
|
+
|
|
45
|
+
path = File.expand_path(string, @root)
|
|
46
|
+
raise Error, 'no such file' unless File.file?(path)
|
|
47
|
+
|
|
48
|
+
from(read(path))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def read(path)
|
|
52
|
+
text = File.read(path, encoding: Encoding::UTF_8)
|
|
53
|
+
|
|
54
|
+
case File.extname(path).downcase
|
|
55
|
+
when '.csv' then CSV.parse(text, skip_blanks: true)
|
|
56
|
+
when '.tsv' then CSV.parse(text, col_sep: "\t", skip_blanks: true)
|
|
57
|
+
when '.json' then JSON.parse(text)
|
|
58
|
+
when '.yml', '.yaml' then YAML.safe_load(text)
|
|
59
|
+
else raise Error, 'only CSV, TSV, JSON and YAML files can be charted'
|
|
60
|
+
end
|
|
61
|
+
rescue CSV::MalformedCSVError, JSON::ParserError, Psych::SyntaxError => e
|
|
62
|
+
raise Error, e.message
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Numbers, rows of hashes like JSON or YAML, or rows of cells like CSV.
|
|
66
|
+
def from_list(list)
|
|
67
|
+
return numbers(list) if list.all? { number(it) }
|
|
68
|
+
return from_table(hash_rows(list), header: true) if list.all?(Hash)
|
|
69
|
+
return from_table(list, header: header?(list.first)) if list.all?(Array)
|
|
70
|
+
|
|
71
|
+
raise Error, 'it needs numbers, or rows with a number in each'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Numbers alone, which have no column to chart against them.
|
|
75
|
+
def numbers(list)
|
|
76
|
+
raise Error, 'minus: needs a table, with a column of numbers to chart against the values' if @minus
|
|
77
|
+
|
|
78
|
+
list.map { [nil, number(it)] }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Rows of hashes as a table, with their keys as its header.
|
|
82
|
+
def hash_rows(rows)
|
|
83
|
+
keys = rows.flat_map(&:keys).uniq
|
|
84
|
+
[keys.map(&:to_s), *rows.map { |row| row.values_at(*keys) }]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# A first row without numbers names the columns, like "month,downloads".
|
|
88
|
+
def header?(row)
|
|
89
|
+
row.none? { number(it) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def from_table(rows, header:)
|
|
93
|
+
names = header ? rows.first.map(&:to_s) : []
|
|
94
|
+
rows = rows.drop(1) if header
|
|
95
|
+
label, value, minus = columns(rows, names)
|
|
96
|
+
|
|
97
|
+
rows.filter_map do |row|
|
|
98
|
+
number = number(row[value])
|
|
99
|
+
[label && row[label].to_s, number, *(number(row[minus]) || 0 if minus)] if number
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The label, value and minus columns' indexes, where a single column of numbers has no labels.
|
|
104
|
+
def columns(rows, names)
|
|
105
|
+
minus = column(@minus, names) if @minus
|
|
106
|
+
value = column(@value, names) if @value
|
|
107
|
+
label = @label ? column(@label, names) : first_label(rows, [value, minus])
|
|
108
|
+
value ||= first_value(rows, [label, minus])
|
|
109
|
+
raise Error, 'there is no column of numbers' unless value
|
|
110
|
+
|
|
111
|
+
[label, value, minus]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The first column that isn't taken, when there's more than one.
|
|
115
|
+
def first_label(rows, taken)
|
|
116
|
+
count = rows.map(&:length).max.to_i
|
|
117
|
+
(0...count).find { !taken.include?(it) } if count > 1
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The first column of numbers that isn't taken.
|
|
121
|
+
def first_value(rows, taken)
|
|
122
|
+
(0...rows.map(&:length).max.to_i).find { |index| !taken.include?(index) && rows.any? { number(it[index]) } }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def column(name, names)
|
|
126
|
+
return name if name.is_a?(Integer)
|
|
127
|
+
|
|
128
|
+
names.index(name.to_s) || raise(Error, "there is no #{name.inspect} column")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def number(value)
|
|
132
|
+
return value if value.is_a?(Numeric)
|
|
133
|
+
return unless value.is_a?(String) && value.strip.match?(NUMBER) && value.match?(/\d/)
|
|
134
|
+
|
|
135
|
+
text = value.strip.delete(',')
|
|
136
|
+
text.include?('.') ? Float(text) : Integer(text, 10)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'drawing'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
class Chart < Node
|
|
7
|
+
# A line of numbers from lowest to highest, or columns a cell high with negative values or a minus column
|
|
8
|
+
# below a line.
|
|
9
|
+
# ▁▃▅█▂
|
|
10
|
+
class Sparkline < Drawing
|
|
11
|
+
SPARKS = %w[▁ ▂ ▃ ▄ ▅ ▆ ▇ █].freeze
|
|
12
|
+
|
|
13
|
+
def lines
|
|
14
|
+
return [@above.map { spark(it, *@above.minmax) }.join] if @below.none?(&:positive?)
|
|
15
|
+
|
|
16
|
+
above, below = grid(1.0 / [@above.max, @below.max].max, slot: 1, gap: 0)
|
|
17
|
+
[*above, '─' * @values.length, *below]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def spark(value, low, high)
|
|
23
|
+
SPARKS[high == low ? 0 : ((value - low) * 7.0 / (high - low)).round]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/raindown/cli.rb
CHANGED
|
@@ -72,7 +72,7 @@ module Raindown
|
|
|
72
72
|
def render_stdin(path, output = nil, bare: false, **)
|
|
73
73
|
Nodes.load(path)
|
|
74
74
|
write(output, Raindown.render(markdown: $stdin.read, metadata: Pages.for(path), bare: bare || BareDirectives.file?(path),
|
|
75
|
-
|
|
75
|
+
**Config.render_options(path)))
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
def watch(path, output = nil, bare: false, execute: false)
|
data/lib/raindown/config.rb
CHANGED
|
@@ -5,9 +5,10 @@ require 'yaml'
|
|
|
5
5
|
module Raindown
|
|
6
6
|
# A project's settings, from config.yml in its .raindown folder:
|
|
7
7
|
# hide_directives: true # Directives become HTML comments rather than bold text in the HTML.
|
|
8
|
+
# highlight_code: false # Code blocks aren't highlighted, so the browser can highlight them instead.
|
|
8
9
|
module Config
|
|
9
10
|
FILE = 'config.yml'
|
|
10
|
-
DEFAULTS = { hide_directives: false }.freeze
|
|
11
|
+
DEFAULTS = { hide_directives: false, highlight_code: true }.freeze
|
|
11
12
|
|
|
12
13
|
module_function
|
|
13
14
|
|
|
@@ -19,5 +20,10 @@ module Raindown
|
|
|
19
20
|
|
|
20
21
|
DEFAULTS.merge(YAML.safe_load_file(file, symbolize_names: true) || {})
|
|
21
22
|
end
|
|
23
|
+
|
|
24
|
+
# The settings for rendering a file, like Raindown.render(**Config.render_options(path)).
|
|
25
|
+
def render_options(path)
|
|
26
|
+
self.for(path).slice(:hide_directives, :highlight_code)
|
|
27
|
+
end
|
|
22
28
|
end
|
|
23
29
|
end
|
data/lib/raindown/node_blocks.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative 'code_blocks'
|
|
|
4
4
|
require_relative 'directives'
|
|
5
5
|
require_relative 'front_matter'
|
|
6
6
|
require_relative 'nodes'
|
|
7
|
+
require_relative 'variables'
|
|
7
8
|
|
|
8
9
|
module Raindown
|
|
9
10
|
# Finds node blocks in a file's lines: a node's directive and the Markdown it wrote below it.
|
|
@@ -25,8 +26,10 @@ module Raindown
|
|
|
25
26
|
SELF_CLOSING = /\A( {0,3})<\{\s*:(#{NAME})(?:\s+(.*?))?\s*\}>[ \t]*$/
|
|
26
27
|
BARE_SELF_CLOSING = /\A( {0,3}):(#{NAME})(?:[ \t]+(.*?))?[ \t]*$/
|
|
27
28
|
# A prop is name=value or name: value, where the value is quoted text, a number, true, false
|
|
28
|
-
# or a
|
|
29
|
+
# or a variable like author.name.
|
|
29
30
|
PROP = /(\w+)(?:=|:[ \t]+)("[^"]*"|'[^']*'|\S+)/
|
|
31
|
+
# A value before the props is the node's argument.
|
|
32
|
+
ARG = /\A("[^"]*"|'[^']*'|[^\s"']\S*)(?:\s+|\z)/
|
|
30
33
|
|
|
31
34
|
module_function
|
|
32
35
|
|
|
@@ -79,7 +82,7 @@ module Raindown
|
|
|
79
82
|
found, pattern = directive(lines[index], bare:)
|
|
80
83
|
return unless found && (klass = Nodes.node_class(found[2]))
|
|
81
84
|
|
|
82
|
-
node = Nodes.instance(klass,
|
|
85
|
+
node = Nodes.instance(klass, *args(found[3].to_s, Variables.new(front_matter)))
|
|
83
86
|
case pattern
|
|
84
87
|
when :open then end_tag(lines, index, found[2]).then { |close| { open: index, close:, last: close, node: } }
|
|
85
88
|
when :bare_open then bare_block(lines, index, node)
|
|
@@ -116,17 +119,25 @@ module Raindown
|
|
|
116
119
|
close
|
|
117
120
|
end
|
|
118
121
|
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
# The props, after a value that's the node's argument when it comes first, like 'data.csv' in chart: 'data.csv'.
|
|
123
|
+
def args(text, variables)
|
|
124
|
+
arg = text.match(ARG) unless text.match?(/\A#{PROP}/)
|
|
125
|
+
return [props(text, variables)] unless arg
|
|
126
|
+
|
|
127
|
+
[props(arg.post_match, variables), [value(arg[1], variables)]]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def props(text, variables)
|
|
131
|
+
text.scan(PROP).to_h { |name, value| [name.to_sym, value(value, variables)] }
|
|
121
132
|
end
|
|
122
133
|
|
|
123
|
-
def value(value,
|
|
134
|
+
def value(value, variables)
|
|
124
135
|
case value
|
|
125
136
|
when /\A(["']).*\1\z/ then value[1..-2]
|
|
126
137
|
when /\A-?\d+\z/ then value.to_i
|
|
127
138
|
when /\A-?\d+\.\d+\z/ then value.to_f
|
|
128
139
|
when 'true', 'false' then value == 'true'
|
|
129
|
-
else value
|
|
140
|
+
else Variables.value(variables, value)
|
|
130
141
|
end
|
|
131
142
|
end
|
|
132
143
|
end
|
data/lib/raindown/nodes.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'chart/chart'
|
|
3
4
|
require_relative 'files'
|
|
4
5
|
require_relative 'node'
|
|
5
6
|
require_relative 'toc'
|
|
@@ -11,8 +12,8 @@ module Raindown
|
|
|
11
12
|
# document: (the Markdown being rendered), metadata: and bare:, and gets the ones it asks for.
|
|
12
13
|
module Nodes
|
|
13
14
|
FOLDER = '.raindown'
|
|
14
|
-
# Lowercase directives for Raindown's own nodes. A project's own TOC or
|
|
15
|
-
BUILT_INS = { 'toc' => 'TOC', 'files' => 'Files' }.freeze
|
|
15
|
+
# Lowercase directives for Raindown's own nodes. A project's own TOC, Files or Chart node replaces Raindown's.
|
|
16
|
+
BUILT_INS = { 'toc' => 'TOC', 'files' => 'Files', 'chart' => 'Chart' }.freeze
|
|
16
17
|
# A placeholder for a node's HTML, which stays as it is through Commonmarker and Antlers.
|
|
17
18
|
PLACEHOLDER = /<!-- raindown:node (\d+) -->/
|
|
18
19
|
|
|
@@ -78,11 +79,11 @@ module Raindown
|
|
|
78
79
|
|
|
79
80
|
# One instance for each node and its props, kept for the whole process.
|
|
80
81
|
# So a node can keep what its Markdown and HTML share, like a date, and both stay the same between renders.
|
|
81
|
-
def instance(klass, props)
|
|
82
|
-
key = [klass, props]
|
|
82
|
+
def instance(klass, props, args = [])
|
|
83
|
+
key = [klass, args, props]
|
|
83
84
|
instances[key] ||= begin
|
|
84
85
|
id = list.length
|
|
85
|
-
list << klass.new(**props)
|
|
86
|
+
list << klass.new(*args, **props)
|
|
86
87
|
id
|
|
87
88
|
end
|
|
88
89
|
|
data/lib/raindown/page.rb
CHANGED
|
@@ -14,12 +14,13 @@ module Raindown
|
|
|
14
14
|
|
|
15
15
|
# Rendered when asked for, so a file that lists itself doesn't render forever.
|
|
16
16
|
# The front matter is rendered with it, so its variables work, and a Raindown file's bare directives too.
|
|
17
|
+
# It follows the file's project config, like render_file, unless the render options say otherwise.
|
|
17
18
|
def content
|
|
18
19
|
@content ||= begin
|
|
19
20
|
markdown = File.read(file_path, encoding: Encoding::UTF_8)
|
|
20
21
|
|
|
21
22
|
if FrontMatter.parse(markdown).last.strip.empty? then ''
|
|
22
|
-
else Raindown.render(markdown:, bare: BareDirectives.file?(file_path),
|
|
23
|
+
else Raindown.render(markdown:, bare: BareDirectives.file?(file_path), **Config.render_options(file_path).merge(@render_options))
|
|
23
24
|
end
|
|
24
25
|
end
|
|
25
26
|
end
|
data/lib/raindown/pages.rb
CHANGED
data/lib/raindown/variables.rb
CHANGED
|
@@ -16,6 +16,21 @@ module Raindown
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# A wrapped value as the data it was, so a variable in a node's props is a hash, list or text like any other.
|
|
20
|
+
def self.unwrap(value)
|
|
21
|
+
case value
|
|
22
|
+
when Variables then value.to_h
|
|
23
|
+
when ::Array then value.map { |item| unwrap(item) }
|
|
24
|
+
when ::String then value.tr(CODE_BRACES.values.join, CODE_BRACES.keys.join)
|
|
25
|
+
else value
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A variable's value, like author.name, read the way {author.name} reads it, unwrapped. Unknown names are nil.
|
|
30
|
+
def self.value(variables, name)
|
|
31
|
+
unwrap(name.split('.').reduce(variables) { |value, key| value.respond_to?(key) ? value.send(key) : nil })
|
|
32
|
+
end
|
|
33
|
+
|
|
19
34
|
def initialize(values)
|
|
20
35
|
@values = values.to_h { |key, value| [key.to_sym, Variables.wrap(value)] }
|
|
21
36
|
end
|
|
@@ -32,6 +47,10 @@ module Raindown
|
|
|
32
47
|
::Kernel.binding
|
|
33
48
|
end
|
|
34
49
|
|
|
50
|
+
def to_h
|
|
51
|
+
@values.transform_values { |value| Variables.unwrap(value) }
|
|
52
|
+
end
|
|
53
|
+
|
|
35
54
|
def to_s
|
|
36
55
|
@values.to_s
|
|
37
56
|
end
|
data/lib/raindown.rb
CHANGED
|
@@ -27,6 +27,8 @@ module Raindown
|
|
|
27
27
|
MARKDOWN_OPTIONS = { extension: { alerts: true, front_matter_delimiter: '---' } }.freeze
|
|
28
28
|
# Directives are HTML comments until Antlers renders them, and escaped characters like "\#" stay plain text.
|
|
29
29
|
HTML_OPTIONS = { render: { unsafe: true, escaped_char_spans: false } }.freeze
|
|
30
|
+
# Without highlighting, code blocks are left for the browser to highlight, like <pre><code class="language-ruby">.
|
|
31
|
+
UNHIGHLIGHTED_OPTIONS = { render: { **HTML_OPTIONS[:render], github_pre_lang: false } }.freeze
|
|
30
32
|
|
|
31
33
|
# Stands in for an app's metadata when there isn't any, so list: and files: list nothing.
|
|
32
34
|
module NoMetadata
|
|
@@ -38,23 +40,41 @@ module Raindown
|
|
|
38
40
|
|
|
39
41
|
module_function
|
|
40
42
|
|
|
41
|
-
# Directives show in bold, unless the file's project
|
|
42
|
-
def render_file(path:, metadata: NoMetadata, bare: false,
|
|
43
|
-
|
|
43
|
+
# Directives show in bold and code is highlighted, unless the file's project changes them in .raindown/config.yml.
|
|
44
|
+
def render_file(path:, metadata: NoMetadata, bare: false, **options)
|
|
45
|
+
options = Config.render_options(path).merge(options)
|
|
46
|
+
render(markdown: File.read(path, encoding: Encoding::UTF_8), metadata:, bare: bare || BareDirectives.file?(path), **options)
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
# Front matter keys are variables, like {title} or <{ title }>.
|
|
47
50
|
# Directives like "toc:" export to HTML as bold or comments depending on hide_directives config.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
# Code blocks are highlighted, unless highlight_code is false for a site that highlights code in the browser.
|
|
52
|
+
def render(markdown:, metadata: NoMetadata, bare: false, hide_directives: false, highlight_code: true)
|
|
53
|
+
highlighting(highlight_code) do
|
|
54
|
+
Fragments.collect do
|
|
55
|
+
# Components' HTML goes back in, then their headings get unique ids, before nodes like the TOC see it.
|
|
56
|
+
html = assembled(markdown:, metadata:, bare:)
|
|
57
|
+
html = Nodes.render_placeholders(html, document: markdown, metadata:, bare:, html:)
|
|
58
|
+
|
|
59
|
+
Directives.render("#{html.chomp}\n", hide: hide_directives)
|
|
60
|
+
end
|
|
55
61
|
end
|
|
56
62
|
end
|
|
57
63
|
|
|
64
|
+
# Whether code is highlighted while the block runs, like one document's render,
|
|
65
|
+
# so components and endpoints rendering their own Markdown follow it too.
|
|
66
|
+
def highlighting(highlight_code)
|
|
67
|
+
outer = Thread.current[:raindown_highlight_code]
|
|
68
|
+
Thread.current[:raindown_highlight_code] = highlight_code
|
|
69
|
+
yield
|
|
70
|
+
ensure
|
|
71
|
+
Thread.current[:raindown_highlight_code] = outer
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def highlight_code?
|
|
75
|
+
Thread.current[:raindown_highlight_code] != false
|
|
76
|
+
end
|
|
77
|
+
|
|
58
78
|
# The document's HTML with its components' HTML put back, and unique heading ids.
|
|
59
79
|
def assembled(markdown:, metadata: NoMetadata, bare: false)
|
|
60
80
|
HeadingIds.unique(Fragments.fill(html(markdown:, variables: variables(markdown:), metadata:, bare:)))
|
|
@@ -71,8 +91,14 @@ module Raindown
|
|
|
71
91
|
doc = Commonmarker.parse(hide_directives(markdown:), options: MARKDOWN_OPTIONS)
|
|
72
92
|
preserve_code(doc:)
|
|
73
93
|
|
|
74
|
-
doc.to_html(options: HTML_OPTIONS)
|
|
75
|
-
|
|
94
|
+
html = highlight_code? ? doc.to_html(options: HTML_OPTIONS) : unhighlighted_html(doc:)
|
|
95
|
+
html.gsub('<!-- ANTLERS', '<{').gsub('ANTLERS -->', '}>')
|
|
96
|
+
.gsub(/[\u{E000}\u{E001}]/, CODE_BRACE_ENTITIES)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Code blocks without highlighting, with a language class for highlighting in the browser.
|
|
100
|
+
def unhighlighted_html(doc:)
|
|
101
|
+
doc.to_html(options: UNHIGHLIGHTED_OPTIONS, plugins: { syntax_highlighter: nil })
|
|
76
102
|
end
|
|
77
103
|
|
|
78
104
|
def variables(markdown:)
|
data/lib/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: raindown
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- maedi
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: antlers
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 1.2.
|
|
18
|
+
version: 1.2.2
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 1.2.
|
|
25
|
+
version: 1.2.2
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: commonmarker
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: csv
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
40
54
|
- !ruby/object:Gem::Dependency
|
|
41
55
|
name: dirload
|
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -154,6 +168,13 @@ files:
|
|
|
154
168
|
- lib/nodes/list_node.rb
|
|
155
169
|
- lib/raindown.rb
|
|
156
170
|
- lib/raindown/bare_directives.rb
|
|
171
|
+
- lib/raindown/chart/bars.rb
|
|
172
|
+
- lib/raindown/chart/chart.rb
|
|
173
|
+
- lib/raindown/chart/columns.rb
|
|
174
|
+
- lib/raindown/chart/drawing.rb
|
|
175
|
+
- lib/raindown/chart/line.rb
|
|
176
|
+
- lib/raindown/chart/points.rb
|
|
177
|
+
- lib/raindown/chart/sparkline.rb
|
|
157
178
|
- lib/raindown/cli.rb
|
|
158
179
|
- lib/raindown/code.rb
|
|
159
180
|
- lib/raindown/code_blocks.rb
|