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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f181e2b7e348aefd77d7f22eef4aee64e16022ba3050d04493b829fbdabff7b
4
- data.tar.gz: 9388ac597b48b784229155e26a96c535b67eb6ff84a277450d1c43d4e117b004
3
+ metadata.gz: c619df9532ee1f014b8837488ac3fe8cc4b3183e7abad17791fb27bb74491499
4
+ data.tar.gz: aad7d7aefc311af905d14afecd4960096b9cebab871c2da957e4ca05cb85c503
5
5
  SHA512:
6
- metadata.gz: 6f0c4bd5677aa6f16058b8528f6af05f4ced2d4f8e4107225ae549e4d1c01e001342e45b8acf3f4e853034692094bcc684c5c8fc89d614ce39651e8307dbafc7
7
- data.tar.gz: e2d10247dc4718f13909849c12feeed14a8c78e73e3de470908a3ed5684b36e0d4663c7cdbfaf45ba10a41a204f169922482739c3c81b49537df17d77a383ed4
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)
@@ -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": "raindown: /notes/a.md: ..."}, like the CLI writes it.
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: "raindown: #{request['path']}: #{e.message}" }
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.mtime(it)] }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Raindown
4
- VERSION = '0.16.1'
4
+ VERSION = '0.17.1'
5
5
  end
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.16.1
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-07 00:00:00.000000000 Z
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