raindown 0.15.0 → 0.16.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 +4 -4
- data/lib/raindown/chart/points.rb +2 -0
- data/lib/raindown/front_matter.rb +10 -0
- data/lib/raindown/list.rb +12 -4
- data/lib/raindown/pages.rb +5 -4
- data/lib/raindown/projects.rb +12 -0
- data/lib/raindown/table/markdown_table.rb +6 -1
- data/lib/raindown/table/table.rb +20 -10
- data/lib/raindown/toc.rb +10 -2
- data/lib/raindown/utils.rb +5 -0
- data/lib/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f181e2b7e348aefd77d7f22eef4aee64e16022ba3050d04493b829fbdabff7b
|
|
4
|
+
data.tar.gz: 9388ac597b48b784229155e26a96c535b67eb6ff84a277450d1c43d4e117b004
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6f0c4bd5677aa6f16058b8528f6af05f4ced2d4f8e4107225ae549e4d1c01e001342e45b8acf3f4e853034692094bcc684c5c8fc89d614ce39651e8307dbafc7
|
|
7
|
+
data.tar.gz: e2d10247dc4718f13909849c12feeed14a8c78e73e3de470908a3ed5684b36e0d4663c7cdbfaf45ba10a41a204f169922482739c3c81b49537df17d77a383ed4
|
|
@@ -5,6 +5,7 @@ require 'json'
|
|
|
5
5
|
require 'yaml'
|
|
6
6
|
|
|
7
7
|
require_relative '../node'
|
|
8
|
+
require_relative '../utils'
|
|
8
9
|
|
|
9
10
|
module Raindown
|
|
10
11
|
class Chart < Node
|
|
@@ -43,6 +44,7 @@ module Raindown
|
|
|
43
44
|
return numbers(numbers) if numbers.any? && numbers.all? { number(it) }
|
|
44
45
|
|
|
45
46
|
path = File.expand_path(string, @root)
|
|
47
|
+
raise Error, "it's outside the project" unless Utils.inside?(path, @root)
|
|
46
48
|
raise Error, 'no such file' unless File.file?(path)
|
|
47
49
|
|
|
48
50
|
from(read(path))
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require 'date'
|
|
4
4
|
require 'yaml'
|
|
5
5
|
|
|
6
|
+
require_relative 'utils'
|
|
7
|
+
|
|
6
8
|
module Raindown
|
|
7
9
|
# YAML between two --- lines at the very start of a file.
|
|
8
10
|
module FrontMatter
|
|
@@ -19,6 +21,14 @@ module Raindown
|
|
|
19
21
|
[metadata.is_a?(Hash) ? metadata : {}, match.post_match]
|
|
20
22
|
end
|
|
21
23
|
|
|
24
|
+
# A file's metadata, or none when it can't be read, with a warning naming it, so one file can't stop a list of them.
|
|
25
|
+
def read(file_path)
|
|
26
|
+
parse(Utils.read_markdown(file_path)).first
|
|
27
|
+
rescue Psych::Exception, ArgumentError => e
|
|
28
|
+
warn "raindown: #{file_path}: #{e.message}, so it's listed without its front matter"
|
|
29
|
+
{}
|
|
30
|
+
end
|
|
31
|
+
|
|
22
32
|
# How many of the lines are front matter, so directives in its YAML are left alone.
|
|
23
33
|
def line_count(lines)
|
|
24
34
|
lines.join.match(PATTERN)&.then { |match| match[0].lines.length } || 0
|
data/lib/raindown/list.rb
CHANGED
|
@@ -14,18 +14,26 @@ module Raindown
|
|
|
14
14
|
@tags = tags
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
# Each file has its front matter, its path and its content. Sorted by an order,
|
|
18
|
-
# with the same value stay in the order they're listed.
|
|
17
|
+
# Each file has its front matter, its path and its content. Sorted by an order, numbers come first, then other values
|
|
18
|
+
# as text, then files without it, and ones with the same value stay in the order they're listed.
|
|
19
19
|
def files(metadata: NoMetadata)
|
|
20
20
|
files = metadata.list(**@tags)
|
|
21
21
|
return files unless @order
|
|
22
22
|
|
|
23
23
|
sorted, unsorted = files.partition { it[@order] }
|
|
24
|
-
sorted.sort_by.with_index { |file, index| [file[@order], index] } + unsorted
|
|
24
|
+
sorted.sort_by.with_index { |file, index| [*order_key(file[@order]), index] } + unsorted
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def rewrite(metadata: NoMetadata)
|
|
28
|
-
files(metadata:).map { |file| "- [#{file.title || file.path}](#{file.path})" }.join("\n")
|
|
28
|
+
files(metadata:).map { |file| "- [#{file.title || File.basename(file.path, '.*')}](#{file.path})" }.join("\n")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def order_key(value)
|
|
34
|
+
return [0, value, ''] if value.is_a?(Numeric)
|
|
35
|
+
|
|
36
|
+
[1, 0, value.to_s]
|
|
29
37
|
end
|
|
30
38
|
end
|
|
31
39
|
end
|
data/lib/raindown/pages.rb
CHANGED
|
@@ -25,10 +25,11 @@ module Raindown
|
|
|
25
25
|
@front_matters ||= {}
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# The Markdown files under a project's root, the folder with its .raindown folder or else the file's
|
|
28
|
+
# The Markdown files under a project's root, the folder with its .raindown folder, or a vault, or else the file's
|
|
29
|
+
# folder.
|
|
29
30
|
def for(path)
|
|
30
|
-
|
|
31
|
-
load(root:
|
|
31
|
+
require_relative 'projects'
|
|
32
|
+
load(root: Projects.content_root(path) || File.dirname(File.expand_path(path)))
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
# Lists the files under the root, without loading them, for Dirload.
|
|
@@ -118,7 +119,7 @@ module Raindown
|
|
|
118
119
|
modified_then, front_matter = self.class.front_matters[file_path]
|
|
119
120
|
return front_matter if modified_then == modified
|
|
120
121
|
|
|
121
|
-
FrontMatter.
|
|
122
|
+
FrontMatter.read(file_path).tap { self.class.front_matters[file_path] = [modified, it] }
|
|
122
123
|
end
|
|
123
124
|
|
|
124
125
|
# The path is relative to the root, like docs/_basics/_1-getting-started.md.
|
data/lib/raindown/projects.rb
CHANGED
|
@@ -44,6 +44,18 @@ module Raindown
|
|
|
44
44
|
folder && File.dirname(folder)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# The folder a file's paths are relative to: the nearest with a .raindown folder or that's a vault, or nil without one.
|
|
48
|
+
# A vault doesn't need a .raindown folder, as Obsidian's links and a table's path are from its top.
|
|
49
|
+
def content_root(path)
|
|
50
|
+
folder = File.dirname(File.expand_path(path))
|
|
51
|
+
until project?(folder) || vault?(folder)
|
|
52
|
+
return if folder == File.dirname(folder)
|
|
53
|
+
|
|
54
|
+
folder = File.dirname(folder)
|
|
55
|
+
end
|
|
56
|
+
folder
|
|
57
|
+
end
|
|
58
|
+
|
|
47
59
|
# The project's Markdown files, sorted, leaving out ignored folders. The projects inside it are left out too,
|
|
48
60
|
# since they have nodes of their own, and vaults, which their editor writes back to.
|
|
49
61
|
def files(root)
|
|
@@ -35,7 +35,12 @@ module Raindown
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def escape(cell)
|
|
38
|
-
cell.gsub('|', '\|')
|
|
38
|
+
one_line(cell).gsub('|', '\|')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The cell as a table shows it, on one line.
|
|
42
|
+
def one_line(cell)
|
|
43
|
+
cell.gsub(/\s*\R\s*/, ' ')
|
|
39
44
|
end
|
|
40
45
|
end
|
|
41
46
|
end
|
data/lib/raindown/table/table.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'digest'
|
|
|
5
5
|
require 'json'
|
|
6
6
|
|
|
7
7
|
require_relative '../node'
|
|
8
|
+
require_relative '../utils'
|
|
8
9
|
require_relative 'markdown_table'
|
|
9
10
|
require_relative 'table_formulas'
|
|
10
11
|
require_relative 'written_table'
|
|
@@ -36,24 +37,31 @@ module Raindown
|
|
|
36
37
|
# Written is what it wrote last time, so it can tell which of the table and the file changed since.
|
|
37
38
|
def rewrite(written: [], metadata: NoMetadata)
|
|
38
39
|
path = data_path(metadata)
|
|
39
|
-
data_error(path) || synced(path, written.map(&:chomp))
|
|
40
|
+
data_error(path, metadata) || synced(path, written.map(&:chomp))
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
# Write-in, in <{ }>, the table only shows its file, and never writes to it.
|
|
43
44
|
def render(metadata: NoMetadata)
|
|
44
45
|
path = data_path(metadata)
|
|
45
|
-
Raindown.commonmark(data_error(path) || MarkdownTable.format(with_total(TableFormulas.evaluate(read_rows(path)))))
|
|
46
|
+
Raindown.commonmark(data_error(path, metadata) || MarkdownTable.format(with_total(TableFormulas.evaluate(read_rows(path)))))
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
private
|
|
49
50
|
|
|
50
51
|
def data_path(metadata)
|
|
51
|
-
File.expand_path(@data.to_s,
|
|
52
|
+
File.expand_path(@data.to_s, data_root(metadata))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def data_root(metadata)
|
|
56
|
+
return metadata.root if metadata.respond_to?(:root)
|
|
57
|
+
|
|
58
|
+
Dir.pwd
|
|
52
59
|
end
|
|
53
60
|
|
|
54
61
|
# Why the file can't be a table, or nil when it can.
|
|
55
|
-
def data_error(path)
|
|
62
|
+
def data_error(path, metadata)
|
|
56
63
|
return "Can't show #{@data.inspect} as a table: only CSV and TSV files can be" unless SEPARATORS.key?(File.extname(path).downcase)
|
|
64
|
+
return "Can't show #{@data.inspect} as a table: it's outside the project" unless Utils.inside?(path, data_root(metadata))
|
|
57
65
|
|
|
58
66
|
"Can't show #{@data.inspect} as a table: no such file" unless File.file?(path)
|
|
59
67
|
end
|
|
@@ -64,7 +72,7 @@ module Raindown
|
|
|
64
72
|
synced_hash = written.first&.[](COMMENT, 1)
|
|
65
73
|
table = WrittenTable.new(written_rows(written), file)
|
|
66
74
|
return markdown(file, table.revealed) if unchanged?(table.rows, synced_hash, shown)
|
|
67
|
-
return markdown(write_rows(path,
|
|
75
|
+
return markdown(write_rows(path, with_file_cells(table.rows, file, shown)), table.revealed) if hash_rows(shown) == synced_hash
|
|
68
76
|
|
|
69
77
|
warn "raindown: #{@data} and its table both changed, so neither is written until they match"
|
|
70
78
|
["<!-- table conflict · #{synced_hash} -->", *written.drop(1)].join("\n")
|
|
@@ -95,12 +103,13 @@ module Raindown
|
|
|
95
103
|
revealed.each_with_object(values.map(&:dup)) { |(row, column), shown| shown[row][column] = rows[row][column] }
|
|
96
104
|
end
|
|
97
105
|
|
|
98
|
-
# The table's rows, keeping the file's
|
|
99
|
-
|
|
106
|
+
# The table's rows, keeping the file's cell where the table still shows it, like a formula as its value, or a cell
|
|
107
|
+
# with line breaks on one line.
|
|
108
|
+
def with_file_cells(table, file, shown)
|
|
100
109
|
table.each_with_index.map do |row, row_index|
|
|
101
110
|
row.each_with_index.map do |cell, column|
|
|
102
|
-
|
|
103
|
-
next
|
|
111
|
+
kept = file.dig(row_index, column)
|
|
112
|
+
next kept if kept && MarkdownTable.one_line(shown.dig(row_index, column).to_s) == cell
|
|
104
113
|
|
|
105
114
|
cell
|
|
106
115
|
end
|
|
@@ -146,8 +155,9 @@ module Raindown
|
|
|
146
155
|
rows
|
|
147
156
|
end
|
|
148
157
|
|
|
158
|
+
# As the table shows them, so a cell with line breaks hashes the same in both.
|
|
149
159
|
def hash_rows(rows)
|
|
150
|
-
Digest::SHA256.hexdigest(JSON.generate(rows))[0, 6]
|
|
160
|
+
Digest::SHA256.hexdigest(JSON.generate(rows.map { |row| row.map { MarkdownTable.one_line(it.to_s) } }))[0, 6]
|
|
151
161
|
end
|
|
152
162
|
end
|
|
153
163
|
end
|
data/lib/raindown/toc.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Raindown
|
|
|
15
15
|
TAG = /<[^>]*>/
|
|
16
16
|
|
|
17
17
|
def rewrite(document:, metadata: nil, bare: false)
|
|
18
|
-
headings =
|
|
18
|
+
headings = headings_below_title(markdown: document, metadata:, bare:)
|
|
19
19
|
return '' if headings.empty?
|
|
20
20
|
|
|
21
21
|
min_level = headings.map { |heading| heading[:level] }.min
|
|
@@ -27,7 +27,7 @@ module Raindown
|
|
|
27
27
|
|
|
28
28
|
# With the document's final HTML, the headings include ones its components render.
|
|
29
29
|
def render(document:, html: nil, metadata: nil, bare: false)
|
|
30
|
-
items =
|
|
30
|
+
items = headings_below_title(markdown: document, html:, metadata:, bare:).map do |heading|
|
|
31
31
|
"<li class='h#{heading[:level]}'><a href='##{heading[:id]}'>#{CGI.escapeHTML(heading[:text])}</a></li>"
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -43,6 +43,14 @@ module Raindown
|
|
|
43
43
|
HTML
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# A lone top-level heading that starts the document is its title, so it's left out when there are headings below it.
|
|
47
|
+
def headings_below_title(**)
|
|
48
|
+
title, *below = headings = headings(**)
|
|
49
|
+
return headings if below.empty? || title[:level] != 1 || below.any? { it[:level] == 1 }
|
|
50
|
+
|
|
51
|
+
below
|
|
52
|
+
end
|
|
53
|
+
|
|
46
54
|
# The document's headings, from its final HTML, or else by rendering it with its components.
|
|
47
55
|
def headings(markdown:, html: nil, metadata: nil, bare: false)
|
|
48
56
|
html ||= Fragments.collect { Raindown.assembled(Context.new(document: markdown, metadata: metadata || NoMetadata, bare:)) }
|
data/lib/raindown/utils.rb
CHANGED
|
@@ -4,6 +4,11 @@ module Raindown
|
|
|
4
4
|
module Utils
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
+
# Whether a path is below a folder, so a note can't reach a file outside its project, like '../../secrets.csv'.
|
|
8
|
+
def inside?(path, folder)
|
|
9
|
+
File.expand_path(path).start_with?(File.join(File.expand_path(folder), ''))
|
|
10
|
+
end
|
|
11
|
+
|
|
7
12
|
# A Markdown file's text, with its line endings as LF, "\n", whatever they are in the file.
|
|
8
13
|
#
|
|
9
14
|
# Windows ends lines with CRLF, "\r\n", and Git for Windows checks files out that way, so a file written or cloned on
|
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.16.1
|
|
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-07 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: antlers
|