raindown 0.16.1 → 0.17.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/cli.rb +13 -5
- data/lib/raindown/server.rb +6 -5
- data/lib/raindown/skeleton.rb +114 -0
- data/lib/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c619df9532ee1f014b8837488ac3fe8cc4b3183e7abad17791fb27bb74491499
|
|
4
|
+
data.tar.gz: aad7d7aefc311af905d14afecd4960096b9cebab871c2da957e4ca05cb85c503
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91c9a12c3bd15d3c9f4ba9e41f645ee5f6ca8447b6208c38d3dd422702acc5f2395161ddb47ae60f248af9707d3c9d53edfa0d4d1dccb78d8aee99987e5c8de3
|
|
7
|
+
data.tar.gz: cd30f02347aae3834e30c0d77e8ee62b74379f005b844ef8c6b6f03f0638ad11ee7da3980b0978c4145b67dbd3144ce395c4abc7c6d8e2f57102a8e2afeb5534
|
data/lib/raindown/cli.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative 'blueprints'
|
|
|
10
10
|
require_relative 'editing'
|
|
11
11
|
require_relative 'rewriter'
|
|
12
12
|
require_relative 'server'
|
|
13
|
+
require_relative 'skeleton'
|
|
13
14
|
require_relative 'watcher/file_watcher'
|
|
14
15
|
require_relative 'watcher/supervisor'
|
|
15
16
|
|
|
@@ -32,6 +33,8 @@ module Raindown
|
|
|
32
33
|
PATH is a file, or a directory (default: current directory) whose projects with a
|
|
33
34
|
.raindown folder are watched
|
|
34
35
|
init [DIRECTORY] Create a .raindown folder for the project's config and nodes (default: current directory)
|
|
36
|
+
skeleton Set this repo up to commit directives without what they write, in files with "skeleton: true"
|
|
37
|
+
skeleton --check Exit with 1, naming any such file with output staged, for CI
|
|
35
38
|
server Keep Raindown loaded for an editor, answering a JSON request a line on stdin with its HTML,
|
|
36
39
|
or its Markdown as it would be written back, like --stdin and --stdin --rewrite
|
|
37
40
|
blueprints Save the blueprints in Claude Code's last reply to .raindown/data/blueprints.md, for the terminal
|
|
@@ -78,17 +81,22 @@ module Raindown
|
|
|
78
81
|
def self.run(argv)
|
|
79
82
|
# Markdown is UTF-8 whatever the locale, as an app opened from the Dock, like Obsidian, starts Raindown without one.
|
|
80
83
|
Encoding.default_external = Encoding::UTF_8
|
|
81
|
-
return if command(argv)
|
|
82
|
-
|
|
83
|
-
# Without a path, like "raindown", "raindown -x" or "raindown watch", it's the current directory.
|
|
84
|
-
argv = ['watch', '.', *argv.drop(1)] if argv.first == 'watch' && (argv.size == 1 || argv[1].start_with?('-'))
|
|
85
|
-
argv = ['.', *argv] if argv.empty? || argv.first.start_with?('-')
|
|
84
|
+
return if command(argv) || Skeleton.run_command(argv)
|
|
86
85
|
|
|
86
|
+
argv = with_path(argv)
|
|
87
87
|
super || abort("Unknown arguments: #{argv.join(' ')}\n\n#{USAGE}")
|
|
88
88
|
rescue StandardError => e
|
|
89
89
|
report(e, path: argv.first)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
# Without a path, like "raindown", "raindown -x" or "raindown watch", it's the current directory.
|
|
93
|
+
def self.with_path(argv)
|
|
94
|
+
return ['watch', '.', *argv.drop(1)] if argv.first == 'watch' && (argv.size == 1 || argv[1].start_with?('-'))
|
|
95
|
+
return ['.', *argv] if argv.empty? || argv.first.start_with?('-')
|
|
96
|
+
|
|
97
|
+
argv
|
|
98
|
+
end
|
|
99
|
+
|
|
92
100
|
# The :path line would match help, version and init as the file, so they're checked first.
|
|
93
101
|
# Returns true when argv was one of them.
|
|
94
102
|
def self.command(argv)
|
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
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
require_relative 'bare_directives'
|
|
6
|
+
require_relative 'document'
|
|
7
|
+
require_relative 'front_matter'
|
|
8
|
+
require_relative 'nodes'
|
|
9
|
+
require_relative 'pages'
|
|
10
|
+
require_relative 'utils'
|
|
11
|
+
|
|
12
|
+
module Raindown
|
|
13
|
+
# A file with "skeleton: true" in its front matter keeps its directives in git, but not what they write, so dashboards
|
|
14
|
+
# don't fill its history and sensitive reports stay out of it. It's a git clean filter: the file on disk keeps its
|
|
15
|
+
# output, and what's committed doesn't, which a clone writes again when it's rendered. The filter's required, so when
|
|
16
|
+
# a file can't be stripped, git stops rather than commit its output.
|
|
17
|
+
module Skeleton
|
|
18
|
+
FILTER = 'raindown'
|
|
19
|
+
SKELETON = /^skeleton:[ \t]*true[ \t]*$/
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# Returns true when the arguments were "raindown skeleton", with --check or a file or neither.
|
|
24
|
+
def run_command(argv)
|
|
25
|
+
return false unless argv.first == 'skeleton' && argv.size <= 2
|
|
26
|
+
|
|
27
|
+
run(argv[1])
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# "raindown skeleton" sets the repo up, --check is for CI, and with a file it's git's filter.
|
|
32
|
+
def run(argument)
|
|
33
|
+
root, = Open3.capture2('git', 'rev-parse', '--show-toplevel')
|
|
34
|
+
abort('raindown skeleton needs a git repo') if root.strip.empty?
|
|
35
|
+
return install(root.strip) unless argument
|
|
36
|
+
return check(root.strip) if argument == '--check'
|
|
37
|
+
|
|
38
|
+
filter(argument)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# From stdin to stdout, which it has to itself, so what nodes print goes to stderr, like the server's answers.
|
|
42
|
+
def filter(path)
|
|
43
|
+
output = $stdout.dup
|
|
44
|
+
$stdout.reopen($stderr)
|
|
45
|
+
output.write(strip($stdin.read, path:))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def check(root)
|
|
49
|
+
files = staged_output(root)
|
|
50
|
+
return if files.empty?
|
|
51
|
+
|
|
52
|
+
warn('Output is staged in these skeleton files. Run "raindown skeleton", then "git add --renormalize .":', *files)
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The Markdown without what its directives wrote, or as it is, to the byte, without "skeleton: true".
|
|
57
|
+
def strip(markdown, path:)
|
|
58
|
+
return markdown unless skeleton?(markdown)
|
|
59
|
+
|
|
60
|
+
FrontMatter.parse(markdown) # Raises when it can't be read, as bare directives would be missed.
|
|
61
|
+
Nodes.load(path)
|
|
62
|
+
markdown = markdown.gsub("\r\n", "\n")
|
|
63
|
+
lines = markdown.lines
|
|
64
|
+
outputs(markdown, path).reverse_each { |output| lines.slice!((output.begin - 1)..(output.end - 1)) }
|
|
65
|
+
lines.join
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Sets the filter up for a repo. Git keeps a filter's command in its own config, so each clone needs this.
|
|
69
|
+
def install(root)
|
|
70
|
+
{ clean: "#{FILTER} skeleton %f", smudge: 'cat', required: 'true' }.each do |name, value|
|
|
71
|
+
system('git', 'config', "filter.#{FILTER}.#{name}", value, chdir: root, exception: true)
|
|
72
|
+
end
|
|
73
|
+
add_attributes(File.join(root, '.gitattributes'))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The staged files that still have output, like ones added before the filter was set up, or in a clone without it.
|
|
77
|
+
def staged_output(root)
|
|
78
|
+
tracked(root).reject do |path|
|
|
79
|
+
staged = staged_markdown(root, path)
|
|
80
|
+
strip(staged, path: File.join(root, path)) == staged
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Read from the front matter's text, so a file with front matter that isn't valid YAML, or isn't UTF-8, can still be
|
|
85
|
+
# committed when it isn't a skeleton.
|
|
86
|
+
def skeleton?(markdown)
|
|
87
|
+
markdown.scrub.match(FrontMatter::PATTERN)&.[](1)&.match?(SKELETON) || false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def outputs(markdown, path)
|
|
91
|
+
bare = BareDirectives.enabled?(path:, content: markdown)
|
|
92
|
+
Document.new(markdown:, bare:).directives.select(&:writes).filter_map(&:output)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def add_attributes(path)
|
|
96
|
+
existing = File.exist?(path) ? File.read(path) : ''
|
|
97
|
+
missing = Pages::EXTENSIONS.map { "*.#{it} filter=#{FILTER}" }.reject { existing.include?(it) }
|
|
98
|
+
return if missing.empty?
|
|
99
|
+
|
|
100
|
+
existing += "\n" unless existing.empty? || existing.end_with?("\n")
|
|
101
|
+
File.write(path, "#{existing}#{missing.join("\n")}\n")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def tracked(root)
|
|
105
|
+
output, = Open3.capture2('git', 'ls-files', '-z', chdir: root)
|
|
106
|
+
output.split("\0").select { Pages::EXTENSIONS.include?(File.extname(it).delete_prefix('.').downcase) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def staged_markdown(root, path)
|
|
110
|
+
output, = Open3.capture2('git', 'show', ":#{path}", chdir: root)
|
|
111
|
+
output.force_encoding(Encoding::UTF_8)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
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.
|
|
4
|
+
version: 0.17.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-08 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: antlers
|
|
@@ -251,6 +251,7 @@ files:
|
|
|
251
251
|
- lib/raindown/projects.rb
|
|
252
252
|
- lib/raindown/rewriter.rb
|
|
253
253
|
- lib/raindown/server.rb
|
|
254
|
+
- lib/raindown/skeleton.rb
|
|
254
255
|
- lib/raindown/stat.rb
|
|
255
256
|
- lib/raindown/table/markdown_table.rb
|
|
256
257
|
- lib/raindown/table/table.rb
|