raindown 0.17.0 → 0.17.2
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/chart.rb +3 -1
- data/lib/raindown/chart/points.rb +1 -1
- data/lib/raindown/cli.rb +3 -2
- data/lib/raindown/editing.rb +1 -1
- data/lib/raindown/node_blocks.rb +34 -7
- data/lib/raindown/node_injector.rb +18 -1
- data/lib/raindown/nodes.rb +1 -0
- data/lib/raindown/problem.rb +34 -0
- data/lib/raindown/rewriter.rb +7 -4
- data/lib/raindown/server.rb +6 -5
- data/lib/raindown/table/table.rb +29 -20
- data/lib/raindown/table/table_file.rb +50 -0
- data/lib/raindown/watcher/file_watcher.rb +1 -0
- data/lib/raindown/watcher/project.rb +7 -1
- data/lib/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce48803b10f9d818849d224607930c3f41c63d66a830e9da2fa6ef43c877a5cb
|
|
4
|
+
data.tar.gz: 040a45c39fc4d1439d80af01c7c6d9d937d24850f69093c9e478c79964305bb6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1e1109ca3ea21b6ae214b5d3bff967d2936e53b23ea0c71b1ca97bbcc208ac4249f5b14d151fb9a94f575d56fbdbfde12ccba1c12bf1b4a287ab632508df1260
|
|
7
|
+
data.tar.gz: 22ba4b827f93a2c9edaf6d21c7be2e8d6916b5a95627e2f29b1309eec01eb6de59cb126ab9874a725ca3b089813529ec7b902501541c06661e5d87922ca9ee41
|
data/lib/raindown/chart/chart.rb
CHANGED
|
@@ -33,12 +33,14 @@ module Raindown
|
|
|
33
33
|
|
|
34
34
|
# The data is read every time, so the chart follows changes to its file.
|
|
35
35
|
def rewrite(metadata: NoMetadata)
|
|
36
|
+
return Problem.alert('Needs a file path to a CSV, TSV, JSON or YAML file') if ['', nil].include?(@data)
|
|
37
|
+
|
|
36
38
|
points = points(metadata:)
|
|
37
39
|
return '' if points.empty?
|
|
38
40
|
|
|
39
41
|
"```\n#{draw(points).map(&:rstrip).join("\n")}\n```"
|
|
40
42
|
rescue Error => e
|
|
41
|
-
"Can't chart #{@data.inspect}: #{e.message}"
|
|
43
|
+
Problem.alert("Can't chart #{@data.inspect}: #{e.message}")
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
# Each value with its label, or nil for numbers alone.
|
data/lib/raindown/cli.rb
CHANGED
|
@@ -169,9 +169,10 @@ module Raindown
|
|
|
169
169
|
Nodes.load(path)
|
|
170
170
|
|
|
171
171
|
metadata = Pages.for(path)
|
|
172
|
-
|
|
172
|
+
rewrite = Rewriter.update(path:, bare:, execute:, metadata:)
|
|
173
|
+
warn(*rewrite.problems.map { "raindown: #{path}: #{it}" })
|
|
173
174
|
write(output, Raindown.render_file(path:, bare:, metadata:))
|
|
174
|
-
check_endpoints(failures)
|
|
175
|
+
check_endpoints(rewrite.failures)
|
|
175
176
|
end
|
|
176
177
|
|
|
177
178
|
# Endpoint responses that fail their checks, like status: 200, are listed, and fail the command, for CI.
|
data/lib/raindown/editing.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Raindown
|
|
|
18
18
|
def rewrite_markdown(markdown, path:, bare: false, execute: false)
|
|
19
19
|
Nodes.load(path)
|
|
20
20
|
rewrite = Rewriter.rewrite(content: markdown, path:, bare:, execute:, metadata: Pages.for(path))
|
|
21
|
-
{ output: rewrite.content, warnings: rewrite.failures.map { "Endpoint check failed: #{it}" } }
|
|
21
|
+
{ output: rewrite.content, warnings: rewrite.failures.map { "Endpoint check failed: #{it}" } + rewrite.problems }
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
end
|
data/lib/raindown/node_blocks.rb
CHANGED
|
@@ -31,13 +31,15 @@ module Raindown
|
|
|
31
31
|
# <{ Card }>, or <{ :Card }>, which a lowercase name like toc needs, as <{ toc }> is a variable.
|
|
32
32
|
SELF_CLOSING = /\A( {0,3})<\{\s*(?::|(?=[A-Z]))(#{NAME})(?:\s+(.*?))?\s*\}>[ \t]*$/
|
|
33
33
|
BARE_SELF_CLOSING = /\A( {0,3}):(#{NAME})(?:[ \t]+(.*?))?[ \t]*$/
|
|
34
|
-
# Unquoted text after name:, like YAML's, runs up to the next prop, so title: Hello world limit: 3 has two.
|
|
35
|
-
|
|
34
|
+
# Unquoted text after name:, like YAML's, runs up to the next prop, so title: Hello world limit: 3 has two. A comma
|
|
35
|
+
# before the next prop, like data.csv, total: true, separates them like a space, while one in text, like Hello,
|
|
36
|
+
# world, is part of it.
|
|
37
|
+
UNQUOTED = /.+?(?=(?:,[ \t]*|[ \t]+)\w+(?:=|:[ \t])|,?[ \t]*\z)/
|
|
36
38
|
# A prop is name=value or name: value, where the value is quoted text, a number, true, false
|
|
37
|
-
# or a variable like author.name. After name= it's one word.
|
|
38
|
-
PROP = /(\w+)(?:=|:[ \t]+)("[^"]*"|'[^']*'|(?<==)\S
|
|
39
|
+
# or a variable like author.name. After name= it's one word, without a comma after it.
|
|
40
|
+
PROP = /(\w+)(?:=|:[ \t]+)("[^"]*"|'[^']*'|(?<==)\S+?(?=,?(?:\s|\z))|#{UNQUOTED})/
|
|
39
41
|
# A value before the props is the node's argument.
|
|
40
|
-
ARG = /\A("[^"]*"|'[^']*'|[^\s"']#{UNQUOTED}|[^\s"'])(
|
|
42
|
+
ARG = /\A("[^"]*"|'[^']*'|[^\s"']#{UNQUOTED}|[^\s"'])(?:,\s*|\s+|\z)/
|
|
41
43
|
|
|
42
44
|
module_function
|
|
43
45
|
|
|
@@ -51,11 +53,33 @@ module Raindown
|
|
|
51
53
|
variables = Variables.new(front_matter)
|
|
52
54
|
|
|
53
55
|
spans.map do |span|
|
|
54
|
-
node =
|
|
56
|
+
node = node_for(span, args(span[:args], variables, antlers: span[:antlers]))
|
|
55
57
|
span.except(:name, :class, :args, :antlers).merge(node:)
|
|
56
58
|
end
|
|
57
59
|
end
|
|
58
60
|
|
|
61
|
+
# The block's node, or a Problem in its place when it can't be made, so the rest of the file still writes back.
|
|
62
|
+
def node_for(span, (props, values))
|
|
63
|
+
problem = values_problem(span, values || [])
|
|
64
|
+
return Nodes.instance(Problem, {}, [problem, span[:open] + 1, span[:name]]) if problem
|
|
65
|
+
|
|
66
|
+
Nodes.instance(span[:class], props, *[values].compact)
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
Nodes.instance(Problem, {}, ["Couldn't start: #{e.message}", span[:open] + 1, span[:name]])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A directive with more or fewer values than its node takes, like toc: extra, rather than Ruby's wrong number of
|
|
72
|
+
# arguments, or nil when it has as many as it takes.
|
|
73
|
+
def values_problem(span, values)
|
|
74
|
+
kinds = span[:class].instance_method(:initialize).parameters.map(&:first)
|
|
75
|
+
required = kinds.count(:req)
|
|
76
|
+
return if kinds.include?(:rest) || values.length.between?(required, required + kinds.count(:opt))
|
|
77
|
+
|
|
78
|
+
return 'Needs a value after it' if values.length < required
|
|
79
|
+
|
|
80
|
+
"Takes no value, so it can't read #{values.first.inspect}"
|
|
81
|
+
end
|
|
82
|
+
|
|
59
83
|
# Each block like find's, with the node's name, class and props text in place of the node,
|
|
60
84
|
# so a block can be found without making its node, like for a document's outline.
|
|
61
85
|
def spans(lines, bare: false)
|
|
@@ -95,10 +119,13 @@ module Raindown
|
|
|
95
119
|
[marker(lines[block[:open]]), *("#{output}\n".lines unless output.empty?), *close]
|
|
96
120
|
end
|
|
97
121
|
|
|
122
|
+
# A node that raises shows why in its place, rather than stopping the page.
|
|
98
123
|
def node_markdown(block, context)
|
|
99
124
|
return Nodes.rendered(block[:node], context) if !block[:writes] || Nodes.html?(block[:node])
|
|
100
125
|
|
|
101
126
|
Nodes.call(block[:node], :rewrite, **context.to_h).to_s
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
Problem.alert("Couldn't write: #{e.message}")
|
|
102
129
|
end
|
|
103
130
|
|
|
104
131
|
def marker(line)
|
|
@@ -155,7 +182,7 @@ module Raindown
|
|
|
155
182
|
|
|
156
183
|
# Text that isn't a prop, like world in title: 'Hello' world or tag=a b, is an error rather than left out.
|
|
157
184
|
def props(text, variables, antlers: true)
|
|
158
|
-
unread = text.gsub(PROP, '').split
|
|
185
|
+
unread = text.gsub(PROP, '').delete(',').split
|
|
159
186
|
raise BrokenText, "Can't read #{unread.join(' ')} in #{text.strip}. Text with spaces needs quotes" if unread.any?
|
|
160
187
|
|
|
161
188
|
text.scan(PROP).to_h { |name, value| [name.to_sym, value(value, variables, antlers:)] }
|
|
@@ -16,8 +16,13 @@ module Raindown
|
|
|
16
16
|
new(content:, path:, bare:, metadata:).call
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Warnings for the directives that couldn't run, written in their place, each with its line, like
|
|
20
|
+
# "line 6: toc: takes no value".
|
|
21
|
+
attr_reader :problems
|
|
22
|
+
|
|
19
23
|
def initialize(content:, path: nil, bare: false, metadata: NoMetadata)
|
|
20
24
|
@content = content
|
|
25
|
+
@problems = []
|
|
21
26
|
@bare = BareDirectives.enabled?(path:, content:, bare:)
|
|
22
27
|
@metadata = metadata
|
|
23
28
|
end
|
|
@@ -28,7 +33,9 @@ module Raindown
|
|
|
28
33
|
.select { |block| block[:writes] }
|
|
29
34
|
return @content if blocks.empty?
|
|
30
35
|
|
|
36
|
+
# From the bottom up, so the lines above each block stay where they are, so its problems are put back in order.
|
|
31
37
|
blocks.reverse_each { |block| lines[block[:open]..block[:last]] = block_lines(lines, block) }
|
|
38
|
+
@problems.reverse!
|
|
32
39
|
lines.join
|
|
33
40
|
end
|
|
34
41
|
|
|
@@ -36,13 +43,23 @@ module Raindown
|
|
|
36
43
|
|
|
37
44
|
def block_lines(lines, block)
|
|
38
45
|
context = { document: @content, metadata: @metadata, bare: @bare, written: written_lines(lines, block) }
|
|
39
|
-
markdown =
|
|
46
|
+
markdown = node_markdown(block, context)
|
|
40
47
|
written = markdown.empty? ? [] : "#{markdown}\n".lines
|
|
41
48
|
return [line(lines[block[:open]]), *bare(written, block)] unless block[:close]
|
|
42
49
|
|
|
43
50
|
delimited(lines, block, written)
|
|
44
51
|
end
|
|
45
52
|
|
|
53
|
+
# A node that raises writes why in its place, like a Problem, so the rest of the file still writes back.
|
|
54
|
+
def node_markdown(block, context)
|
|
55
|
+
@problems << block[:node].warning if block[:node].is_a?(Problem)
|
|
56
|
+
Nodes.call(block[:node], :rewrite, **context).to_s.chomp
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
problem = Problem.new("Couldn't write: #{e.message}", block[:open] + 1)
|
|
59
|
+
@problems << problem.warning
|
|
60
|
+
problem.rewrite
|
|
61
|
+
end
|
|
62
|
+
|
|
46
63
|
# The directive, what the node wrote and the end tag, straight after each other, as a comment on its own line ends
|
|
47
64
|
# at its -->, so what's written below it, like a list, is its own block.
|
|
48
65
|
def delimited(lines, block, written) = [line(lines[block[:open]]), *written, line(lines[block[:close]])]
|
data/lib/raindown/nodes.rb
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'node'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
# What a directive writes in place of its output when it can't run, like toc: extra, or a node that raised, so the
|
|
7
|
+
# rest of the file still writes back. In the file it's under its directive, so only its warning says its line and
|
|
8
|
+
# name. It never starts with the name, like toc:, which would read as a directive once the one above it's deleted.
|
|
9
|
+
class Problem < Node
|
|
10
|
+
attr_reader :message, :line
|
|
11
|
+
|
|
12
|
+
def initialize(message, line, name = nil)
|
|
13
|
+
@message = message
|
|
14
|
+
@line = line
|
|
15
|
+
@name = name
|
|
16
|
+
super()
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A message as a Markdown alert, which Obsidian, GitHub and Raindown show as a warning, and which can't read as a
|
|
20
|
+
# directive, as it starts with >.
|
|
21
|
+
def self.alert(message)
|
|
22
|
+
"> [!WARNING]\n#{message.lines.map { "> #{it}" }.join}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def rewrite
|
|
26
|
+
self.class.alert(@message)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# For a terminal, or an editor to underline the line, like "line 6: toc: takes no value".
|
|
30
|
+
def warning
|
|
31
|
+
["line #{@line}", @name, @message.sub(/\A[A-Z](?![A-Z])/, &:downcase)].compact.join(': ')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/raindown/rewriter.rb
CHANGED
|
@@ -9,8 +9,9 @@ module Raindown
|
|
|
9
9
|
# Rewrites the generated parts of a Markdown file in place before it's rendered:
|
|
10
10
|
# node output, like the TOC, endpoint responses and table alignment.
|
|
11
11
|
module Rewriter
|
|
12
|
-
# The content written back,
|
|
13
|
-
|
|
12
|
+
# The content written back, the endpoint checks it failed, like "api.md:3: expected status 200, received 404", and
|
|
13
|
+
# the directives that couldn't run, like "line 4: toc: takes no value", written in their place.
|
|
14
|
+
Rewrite = Data.define(:original, :content, :failures, :problems) do
|
|
14
15
|
def changed?
|
|
15
16
|
content != original
|
|
16
17
|
end
|
|
@@ -19,9 +20,11 @@ module Raindown
|
|
|
19
20
|
module_function
|
|
20
21
|
|
|
21
22
|
def rewrite(content:, path:, bare: false, execute: false, metadata: NoMetadata)
|
|
22
|
-
|
|
23
|
+
injector = NodeInjector.new(content:, path:, bare:, metadata:)
|
|
24
|
+
injected = injector.call
|
|
23
25
|
endpoints = EndpointRunner.new(content: injected, path:, bare: BareDirectives.enabled?(path:, content: injected, bare:), execute:)
|
|
24
|
-
Rewrite.new(original: content, content: TableFormatter.rewrite(content: endpoints.rewrite), failures: endpoints.failures
|
|
26
|
+
Rewrite.new(original: content, content: TableFormatter.rewrite(content: endpoints.rewrite), failures: endpoints.failures,
|
|
27
|
+
problems: injector.problems)
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
# Writes the file only when it changed.
|
data/lib/raindown/server.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Raindown
|
|
|
13
13
|
# {"id": 1, "path": "/notes/a.md", "markdown": "<!-- toc: -->", "rewrite": true}
|
|
14
14
|
# {"id": 1, "output": "<!-- toc: -->\n- ...", "warnings": []}
|
|
15
15
|
# Rewriting answers like --stdin --rewrite, with "execute" like -x, and otherwise it's HTML, like --stdin. An error's
|
|
16
|
-
# answered as {"id": 1, "error": "
|
|
16
|
+
# answered as {"id": 1, "error": "/notes/a.md: ..."}, like the CLI writes it.
|
|
17
17
|
# Each project renders in a worker of its own, which starts again when its .raindown folder changes, so its nodes,
|
|
18
18
|
# config and schemas are never stale. Without fork, as on Windows, node changes need the server started again.
|
|
19
19
|
class Server
|
|
@@ -33,13 +33,14 @@ module Raindown
|
|
|
33
33
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
|
+
# Any error is the answer, as an editor's only got its server, and one note can't be allowed to stop it.
|
|
36
37
|
def answer(line)
|
|
37
38
|
request = JSON.parse(line)
|
|
38
39
|
{ id: request['id'], **edit(request) }
|
|
39
|
-
rescue JSON::ParserError, KeyError => e
|
|
40
|
-
{ id: request&.dig('id'), error: "raindown: #{e.message}" }
|
|
41
40
|
rescue Workers::Worker::Error => e
|
|
42
|
-
{ id: request['id'], error: "
|
|
41
|
+
{ id: request['id'], error: "#{request['path']}: #{e.message}" }
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
{ id: (request['id'] if request.is_a?(Hash)), error: e.message.to_s }
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def edit(request)
|
|
@@ -59,7 +60,7 @@ module Raindown
|
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def nodes_changed?(project)
|
|
62
|
-
stamp = Dir.glob(File.join(project, Nodes::FOLDER, '**', '*')).to_h { [it, File.
|
|
63
|
+
stamp = Dir.glob(File.join(project, Nodes::FOLDER, '**', '*')).to_h { [it, File.lstat(it).mtime] }
|
|
63
64
|
changed = @stamps.key?(project) && @stamps[project] != stamp
|
|
64
65
|
@stamps[project] = stamp
|
|
65
66
|
changed
|
data/lib/raindown/table/table.rb
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'csv'
|
|
4
3
|
require 'digest'
|
|
5
4
|
require 'json'
|
|
6
5
|
|
|
7
6
|
require_relative '../node'
|
|
8
7
|
require_relative '../utils'
|
|
9
8
|
require_relative 'markdown_table'
|
|
9
|
+
require_relative 'table_file'
|
|
10
10
|
require_relative 'table_formulas'
|
|
11
11
|
require_relative 'written_table'
|
|
12
12
|
|
|
@@ -26,24 +26,31 @@ module Raindown
|
|
|
26
26
|
# the padding around one's value and it shows its formula, to edit, until it's changed.
|
|
27
27
|
class Table < Node
|
|
28
28
|
COMMENT = /\A<!-- table (?:conflict|\d+ rows?) · (\h+) -->\z/
|
|
29
|
-
SEPARATORS = { '.csv' => ',', '.tsv' => "\t" }.freeze
|
|
30
29
|
BOLD = /\A\*\*.*\*\*\z/
|
|
30
|
+
# The table a new file starts with, when there's no table to make it from.
|
|
31
|
+
STARTER = [['Column 1', 'Column 2'], ['', '']].freeze
|
|
31
32
|
|
|
32
|
-
def initialize(data, total: false)
|
|
33
|
+
def initialize(data = nil, total: false)
|
|
33
34
|
@data = data
|
|
34
35
|
@total = total
|
|
35
36
|
end
|
|
36
37
|
|
|
37
|
-
# Written is what it wrote last time, so it can tell which of the table and the file changed since.
|
|
38
|
+
# Written is what it wrote last time, so it can tell which of the table and the file changed since. A file that's
|
|
39
|
+
# not there yet is made, from the table below it, or else one to fill in.
|
|
38
40
|
def rewrite(written: [], metadata: NoMetadata)
|
|
39
41
|
path = data_path(metadata)
|
|
40
|
-
|
|
42
|
+
lines = written.map(&:chomp)
|
|
43
|
+
create_file(path, lines) if missing_file?(path, metadata)
|
|
44
|
+
data_error(path, metadata)&.then { Problem.alert(it) } || synced(path, lines)
|
|
41
45
|
end
|
|
42
46
|
|
|
43
47
|
# Write-in, in <{ }>, the table only shows its file, and never writes to it.
|
|
44
48
|
def render(metadata: NoMetadata)
|
|
45
49
|
path = data_path(metadata)
|
|
46
|
-
|
|
50
|
+
error = data_error(path, metadata)
|
|
51
|
+
return Raindown.commonmark(Problem.alert(error)) if error
|
|
52
|
+
|
|
53
|
+
Raindown.commonmark(MarkdownTable.format(with_total(TableFormulas.evaluate(TableFile.read(path)))))
|
|
47
54
|
end
|
|
48
55
|
|
|
49
56
|
private
|
|
@@ -58,21 +65,34 @@ module Raindown
|
|
|
58
65
|
Dir.pwd
|
|
59
66
|
end
|
|
60
67
|
|
|
68
|
+
def missing_file?(path, metadata)
|
|
69
|
+
TableFile.supported?(path) && Utils.inside?(path, data_root(metadata)) && !File.exist?(path)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# From the table as it's written, which has no comment yet when it's just been typed, below any alert saying why
|
|
73
|
+
# there was no file.
|
|
74
|
+
def create_file(path, lines)
|
|
75
|
+
rows = MarkdownTable.parse(lines.grep_v(COMMENT).grep_v(/\A>/))
|
|
76
|
+
rows = STARTER if rows.empty?
|
|
77
|
+
TableFile.create(path, rows)
|
|
78
|
+
end
|
|
79
|
+
|
|
61
80
|
# Why the file can't be a table, or nil when it can.
|
|
62
81
|
def data_error(path, metadata)
|
|
63
|
-
return
|
|
82
|
+
return 'Needs a file path to a CSV/TSV that exists or will be created' if @data.to_s.strip.empty?
|
|
83
|
+
return "#{@data.inspect} must be a file path to a CSV/TSV that exists or will be created" unless TableFile.supported?(path)
|
|
64
84
|
return "Can't show #{@data.inspect} as a table: it's outside the project" unless Utils.inside?(path, data_root(metadata))
|
|
65
85
|
|
|
66
86
|
"Can't show #{@data.inspect} as a table: no such file" unless File.file?(path)
|
|
67
87
|
end
|
|
68
88
|
|
|
69
89
|
def synced(path, written)
|
|
70
|
-
file =
|
|
90
|
+
file = TableFile.read(path)
|
|
71
91
|
shown = TableFormulas.evaluate(file)
|
|
72
92
|
synced_hash = written.first&.[](COMMENT, 1)
|
|
73
93
|
table = WrittenTable.new(written_rows(written), file)
|
|
74
94
|
return markdown(file, table.revealed) if unchanged?(table.rows, synced_hash, shown)
|
|
75
|
-
return markdown(
|
|
95
|
+
return markdown(TableFile.write(path, with_file_cells(table.rows, file, shown)), table.revealed) if hash_rows(shown) == synced_hash
|
|
76
96
|
|
|
77
97
|
warn "raindown: #{@data} and its table both changed, so neither is written until they match"
|
|
78
98
|
["<!-- table conflict · #{synced_hash} -->", *written.drop(1)].join("\n")
|
|
@@ -144,17 +164,6 @@ module Raindown
|
|
|
144
164
|
row.any? { !it.empty? } && row.all? { it.empty? || it.match?(BOLD) }
|
|
145
165
|
end
|
|
146
166
|
|
|
147
|
-
def read_rows(path)
|
|
148
|
-
rows = CSV.read(path, col_sep: SEPARATORS[File.extname(path).downcase], skip_blanks: true, encoding: Encoding::UTF_8)
|
|
149
|
-
width = rows.map(&:length).max.to_i
|
|
150
|
-
rows.map { |row| Array.new(width) { row[it].to_s.strip } }
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def write_rows(path, rows)
|
|
154
|
-
File.write(path, rows.map { CSV.generate_line(it, col_sep: SEPARATORS[File.extname(path).downcase]) }.join)
|
|
155
|
-
rows
|
|
156
|
-
end
|
|
157
|
-
|
|
158
167
|
# As the table shows them, so a cell with line breaks hashes the same in both.
|
|
159
168
|
def hash_rows(rows)
|
|
160
169
|
Digest::SHA256.hexdigest(JSON.generate(rows.map { |row| row.map { MarkdownTable.one_line(it.to_s) } }))[0, 6]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'csv'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Raindown
|
|
7
|
+
# The CSV or TSV file behind a table:, as rows of cells as wide as each other, written back in its own format.
|
|
8
|
+
module TableFile
|
|
9
|
+
SEPARATORS = { '.csv' => ',', '.tsv' => "\t" }.freeze
|
|
10
|
+
BYTE_ORDER_MARK = "\uFEFF"
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def supported?(path)
|
|
15
|
+
SEPARATORS.key?(File.extname(path).downcase)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def read(path)
|
|
19
|
+
rows = CSV.read(path, col_sep: separator(path), skip_blanks: true, encoding: 'bom|utf-8')
|
|
20
|
+
width = rows.map(&:length).max.to_i
|
|
21
|
+
rows.map { |row| Array.new(width) { row[it].to_s.strip } }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A new file, and the folders it's in, which has no format of its own to keep yet.
|
|
25
|
+
def create(path, rows)
|
|
26
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
27
|
+
File.binwrite(path, rows.map { CSV.generate_line(without_empty_cells(it), col_sep: separator(path)) }.join)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Keeps the file's line endings and any byte order mark, like Excel's.
|
|
31
|
+
def write(path, rows)
|
|
32
|
+
text = File.binread(path).force_encoding(Encoding::UTF_8)
|
|
33
|
+
options = { col_sep: separator(path), row_sep: text[/\r?\n/] || "\n" }
|
|
34
|
+
lines = rows.map { CSV.generate_line(without_empty_cells(it), **options) }
|
|
35
|
+
File.binwrite(path, [(BYTE_ORDER_MARK if text.start_with?(BYTE_ORDER_MARK)), *lines].join)
|
|
36
|
+
rows
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def separator(path)
|
|
40
|
+
SEPARATORS[File.extname(path).downcase]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Empty cells as nil, which CSV leaves empty rather than quoting, but for a row of one, as it would be a blank line.
|
|
44
|
+
def without_empty_cells(row)
|
|
45
|
+
return row if row.one?
|
|
46
|
+
|
|
47
|
+
row.map { it unless it.empty? }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -53,6 +53,7 @@ module Raindown
|
|
|
53
53
|
def rewrite
|
|
54
54
|
rewrite = Rewriter.update(path:, bare:, execute:, metadata: Pages.for(path))
|
|
55
55
|
rewrite.failures.each { warn "Endpoint check failed: #{it}" }
|
|
56
|
+
rewrite.problems.each { warn "raindown: #{path}: #{it}" }
|
|
56
57
|
rewrite.changed?
|
|
57
58
|
end
|
|
58
59
|
end
|
|
@@ -30,7 +30,7 @@ module Raindown
|
|
|
30
30
|
def update(path)
|
|
31
31
|
rewrite = Rewriter.update(path:, bare: @bare, execute: @execute, metadata: Pages.for(path))
|
|
32
32
|
puts "[#{name}] Updated #{relative(path)}" if rewrite.changed?
|
|
33
|
-
rewrite
|
|
33
|
+
report(rewrite, path)
|
|
34
34
|
rescue StandardError => e
|
|
35
35
|
raise if ENV['RAINDOWN_DEBUG']
|
|
36
36
|
|
|
@@ -40,6 +40,12 @@ module Raindown
|
|
|
40
40
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
|
+
# What writing back found wrong: failed endpoint checks, and directives that couldn't run, written in their place.
|
|
44
|
+
def report(rewrite, path)
|
|
45
|
+
rewrite.failures.each { warn "[#{name}] Endpoint check failed: #{it}" }
|
|
46
|
+
rewrite.problems.each { warn "[#{name}] raindown: #{relative(path)}: #{it}" }
|
|
47
|
+
end
|
|
48
|
+
|
|
43
49
|
def relative(path)
|
|
44
50
|
File.expand_path(path).delete_prefix(File.join(root, ''))
|
|
45
51
|
end
|
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.17.
|
|
4
|
+
version: 0.17.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- maedi
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-11-
|
|
10
|
+
date: 2026-11-08 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: antlers
|
|
@@ -248,6 +248,7 @@ files:
|
|
|
248
248
|
- lib/raindown/page.rb
|
|
249
249
|
- lib/raindown/pages.rb
|
|
250
250
|
- lib/raindown/pre_renderer.rb
|
|
251
|
+
- lib/raindown/problem.rb
|
|
251
252
|
- lib/raindown/projects.rb
|
|
252
253
|
- lib/raindown/rewriter.rb
|
|
253
254
|
- lib/raindown/server.rb
|
|
@@ -255,6 +256,7 @@ files:
|
|
|
255
256
|
- lib/raindown/stat.rb
|
|
256
257
|
- lib/raindown/table/markdown_table.rb
|
|
257
258
|
- lib/raindown/table/table.rb
|
|
259
|
+
- lib/raindown/table/table_file.rb
|
|
258
260
|
- lib/raindown/table/table_formatter.rb
|
|
259
261
|
- lib/raindown/table/table_formulas.rb
|
|
260
262
|
- lib/raindown/table/written_table.rb
|