raindown 0.10.0 → 0.12.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.
@@ -8,26 +8,27 @@ require_relative 'utils'
8
8
  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
- # Each rewriter responds to rewrite(content:, path:, bare:, execute:, metadata:) and returns the new content.
12
11
  module Rewriter
13
- REWRITERS = [NodeInjector, EndpointRunner, TableFormatter].freeze
12
+ # The content written back, and the endpoint checks it failed, like "api.md:3: expected status 200, received 404".
13
+ Rewrite = Data.define(:original, :content, :failures) do
14
+ def changed?
15
+ content != original
16
+ end
17
+ end
14
18
 
15
19
  module_function
16
20
 
17
21
  def rewrite(content:, path:, bare: false, execute: false, metadata: NoMetadata)
18
- REWRITERS.reduce(content) do |rewritten, rewriter|
19
- rewriter.rewrite(content: rewritten, path:, bare:, execute:, metadata:)
20
- end
22
+ injected = NodeInjector.rewrite(content:, path:, bare:, metadata:)
23
+ 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)
21
25
  end
22
26
 
23
- # Returns true when the file changed.
27
+ # Writes the file only when it changed.
24
28
  def update(path:, bare: false, execute: false, metadata: NoMetadata)
25
- content = Utils.read_markdown(path)
26
- rewritten = rewrite(content:, path:, bare:, execute:, metadata:)
27
- return false if rewritten == content
28
-
29
- File.write(path, rewritten)
30
- true
29
+ rewrite(content: Utils.read_markdown(path), path:, bare:, execute:, metadata:).tap do |rewrite|
30
+ File.write(path, rewrite.content) if rewrite.changed?
31
+ end
31
32
  end
32
33
  end
33
34
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative '../version'
5
+ require_relative 'editing'
6
+ require_relative 'projects'
7
+ require_relative 'workers/workers'
8
+
9
+ module Raindown
10
+ # "raindown server" keeps Raindown loaded for editors, like the Obsidian plugin and the VS Code extension, so a render
11
+ # doesn't start Ruby again. Once it's ready, it says so, then answers a JSON request a line, in order:
12
+ # {"ready": true, "version": "0.10.0"}
13
+ # {"id": 1, "path": "/notes/a.md", "markdown": "<!-- toc: -->", "rewrite": true}
14
+ # {"id": 1, "output": "<!-- toc: -->\n- ...", "warnings": []}
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.
17
+ # Each project renders in a worker of its own, which starts again when its .raindown folder changes, so its nodes,
18
+ # config and schemas are never stale. Without fork, as on Windows, node changes need the server started again.
19
+ class Server
20
+ def initialize(input: $stdin, output: $stdout)
21
+ @input = input
22
+ @output = output
23
+ @workers = Workers.new(root: Dir.pwd) { Editing }
24
+ @stamps = {}
25
+ end
26
+
27
+ def start
28
+ write(ready: true, version: VERSION)
29
+ @input.each_line { |line| write(answer(line)) }
30
+ ensure
31
+ @workers.stop
32
+ end
33
+
34
+ private
35
+
36
+ def answer(line)
37
+ request = JSON.parse(line)
38
+ { id: request['id'], **edit(request) }
39
+ rescue JSON::ParserError, KeyError => e
40
+ { id: request&.dig('id'), error: "raindown: #{e.message}" }
41
+ rescue Workers::Worker::Error => e
42
+ { id: request['id'], error: "raindown: #{request['path']}: #{e.message}" }
43
+ end
44
+
45
+ def edit(request)
46
+ path = File.expand_path(request.fetch('path'))
47
+ options = { path:, bare: request['bare'] == true }
48
+ worker = worker_for(path)
49
+ return worker.call(:rewrite_markdown, request.fetch('markdown'), **options, execute: request['execute'] == true) if request['rewrite']
50
+
51
+ { output: worker.call(:render_markdown, request.fetch('markdown'), **options) }
52
+ end
53
+
54
+ # The project's worker, started again first when a file in its .raindown folder was added, changed or removed.
55
+ def worker_for(path)
56
+ project = Projects.root(path) || @workers.root
57
+ @workers.restart(project) if nodes_changed?(project)
58
+ @workers[project]
59
+ end
60
+
61
+ def nodes_changed?(project)
62
+ stamp = Dir.glob(File.join(project, Nodes::FOLDER, '**', '*')).to_h { [it, File.mtime(it)] }
63
+ changed = @stamps.key?(project) && @stamps[project] != stamp
64
+ @stamps[project] = stamp
65
+ changed
66
+ end
67
+
68
+ def write(message)
69
+ @output.puts(JSON.generate(message))
70
+ @output.flush
71
+ end
72
+ end
73
+ end
@@ -11,7 +11,7 @@ require_relative 'filter'
11
11
 
12
12
  module Raindown
13
13
  module TUI
14
- # The blueprints from Claude Code's replies, in the project's .raindown/blueprints.md, newest first, then those in its
14
+ # The blueprints from Claude Code's replies, in the project's .raindown/data/blueprints.md, newest first, then those in its
15
15
  # code's comments, one at a time, of
16
16
  # those its Filter's keywords match, each traced to its code: the arrows move between the boxes drawn in one, or a
17
17
  # click on one, the code each box names shows below, Space or another click moving on to
@@ -22,16 +22,16 @@ module Raindown
22
22
  end
23
23
  end
24
24
 
25
- # The feelings everyone's sorted about the code as it is now, kept in .raindown/feelings.csv with the commit they were
25
+ # The feelings everyone's sorted about the code as it is now, kept in .raindown/data/feelings.csv with the commit they were
26
26
  # felt at, which a node can count, like the README's code debt. A feeling about code that's since changed, or gone, moves to
27
- # .raindown/memories.csv, so the feelings stay few, and the history's kept.
27
+ # .raindown/data/memories.csv, so the feelings stay few, and the history's kept.
28
28
  class FeelingsLog
29
29
  TEMPERS = %w[frolic numb woe dread malice].freeze
30
30
  HEADER = %i[timestamp commit checksum location method temper note].freeze
31
31
  def initialize(root:)
32
32
  @root = root
33
- @csv = File.join(root, '.raindown', 'feelings.csv')
34
- @memories = File.join(root, '.raindown', 'memories.csv')
33
+ @csv = File.join(root, '.raindown', 'data', 'feelings.csv')
34
+ @memories = File.join(root, '.raindown', 'data', 'memories.csv')
35
35
  end
36
36
 
37
37
  # Adds the feeling, as the newest, in place of one about the method before, which moves to the memories if it was
@@ -90,8 +90,9 @@ module Raindown
90
90
 
91
91
  def endpoint(path, lines, directive)
92
92
  snippet = CodeBlocks.endpoint(lines, directive.line).first&.dig(:body)
93
- request = Curl.request(snippet.to_s, EndpointVariables.new(path:, content: lines.join))
94
- Endpoint.new(project: root, path:, line: directive.line, **request.to_h) if request
93
+ variables = EndpointVariables.new(path:, content: lines.join)
94
+ request = Curl.request(snippet.to_s, variables)
95
+ Endpoint.new(project: root, path:, line: directive.line, method: request.method, url: variables.redact(request.url)) if request
95
96
  end
96
97
 
97
98
  def charts_in(path)
@@ -51,7 +51,9 @@ module Raindown
51
51
 
52
52
  # The files' metadata is loaded again each time, so new and renamed files are listed.
53
53
  def rewrite
54
- Rewriter.update(path:, bare:, execute:, metadata: Pages.for(path))
54
+ rewrite = Rewriter.update(path:, bare:, execute:, metadata: Pages.for(path))
55
+ rewrite.failures.each { warn "Endpoint check failed: #{it}" }
56
+ rewrite.changed?
55
57
  end
56
58
  end
57
59
  end
@@ -28,8 +28,9 @@ module Raindown
28
28
 
29
29
  # Writing back saves the file again, and that save changes nothing, so it ends there.
30
30
  def update(path)
31
- changed = Rewriter.update(path:, bare: @bare, execute: @execute, metadata: Pages.for(path))
32
- puts "[#{name}] Updated #{relative(path)}" if changed
31
+ rewrite = Rewriter.update(path:, bare: @bare, execute: @execute, metadata: Pages.for(path))
32
+ puts "[#{name}] Updated #{relative(path)}" if rewrite.changed?
33
+ rewrite.failures.each { warn "[#{name}] Endpoint check failed: #{it}" }
33
34
  rescue StandardError => e
34
35
  raise if ENV['RAINDOWN_DEBUG']
35
36
 
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'code'
4
+ require_relative 'front_matter'
5
+
6
+ module Raindown
7
+ # Obsidian's links between notes, which render as Markdown links to the file they name, by its path in the project,
8
+ # like files: links, while the file keeps them as they are:
9
+ # [[Reading list]] [Reading list](<notes/Reading list.md>)
10
+ # [[Reading list|books]] [books](<notes/Reading list.md>)
11
+ # [[Reading list#To read]] [Reading list > To read](<notes/Reading list.md#to-read>)
12
+ # A name matches a file's name, or the end of its path, like [[notes/Reading list]], without its extension and
13
+ # whatever its case. One that matches no file, and ones in code or front matter, are left as they are, as are
14
+ # embeds, like ![[photo.png]].
15
+ module WikiLinks
16
+ LINK = /(?<!!)\[\[([^\[\]|#\n]+)(?:#([^\[\]|\n]+))?(?:\|([^\[\]\n]+))?\]\]/
17
+
18
+ module_function
19
+
20
+ def to_markdown(markdown, metadata:)
21
+ return markdown unless markdown.include?('[[')
22
+
23
+ pages = metadata.list
24
+ links(markdown).reverse_each.with_object(markdown.b) do |link, bytes|
25
+ page = page_for(link[1], pages)
26
+ bytes[link.byteoffset(0)[0]...link.byteoffset(0)[1]] = markdown_link(link, page).b if page
27
+ end.force_encoding('UTF-8')
28
+ end
29
+
30
+ # The links in the Markdown, outside its code and front matter, as matches.
31
+ def links(markdown)
32
+ start = markdown.bytesize - FrontMatter.parse(markdown).last.bytesize
33
+ code = Code.ranges(markdown)
34
+ found = []
35
+
36
+ markdown.scan(LINK) do
37
+ link = ::Regexp.last_match
38
+ offset = link.byteoffset(0)[0]
39
+ found << link if offset >= start && code.none? { it.cover?(offset) }
40
+ end
41
+
42
+ found
43
+ end
44
+
45
+ # The page a name links to: one whose path without its extension is the name, or ends with it.
46
+ def page_for(name, pages)
47
+ name = name.strip.downcase.sub(/\.(?:md|rd|markdown|raindown)\z/, '')
48
+ stems = pages.to_h { [it, it.path.to_s.downcase.sub(%r{\.[^./]+\z}, '')] }
49
+
50
+ pages.find { stems[it] == name } || pages.find { stems[it].end_with?("/#{name}") }
51
+ end
52
+
53
+ def markdown_link(link, page)
54
+ name, heading, label = link.captures.map { it&.strip }
55
+ label ||= [name, heading].compact.join(' > ')
56
+ anchor = "##{anchor(heading)}" if heading
57
+
58
+ "[#{label.gsub(/[\[\]]/) { "\\#{it}" }}](<#{page.path}#{anchor}>)"
59
+ end
60
+
61
+ # A heading's id, the way Commonmarker makes it, like to-read for "To read".
62
+ def anchor(heading)
63
+ heading.downcase.gsub(/[^\p{Word}\- ]/, '').tr(' ', '-')
64
+ end
65
+ end
66
+ end
data/lib/raindown.rb CHANGED
@@ -17,6 +17,7 @@ require_relative 'raindown/pages'
17
17
  require_relative 'raindown/pre_renderer'
18
18
  require_relative 'raindown/utils'
19
19
  require_relative 'raindown/variables'
20
+ require_relative 'raindown/wiki_links'
20
21
 
21
22
  module Raindown
22
23
  # Antlers evaluates any braces in the HTML, so braces in code are swapped for placeholders
@@ -83,6 +84,7 @@ module Raindown
83
84
  # The document's HTML from Commonmarker, after nodes and the template pass, with components as fragments.
84
85
  def html(markdown:, variables:, metadata: NoMetadata, bare: false)
85
86
  markdown = NodeBlocks.to_rendered(markdown.dup.force_encoding('UTF-8'), metadata:, bare:)
87
+ markdown = WikiLinks.to_markdown(markdown, metadata:)
86
88
  commonmark(PreRenderer.render(markdown, variables:, metadata:, bare:))
87
89
  end
88
90
 
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Raindown
4
- VERSION = '0.10.0'
4
+ VERSION = '0.12.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raindown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -121,6 +121,20 @@ dependencies:
121
121
  - - ">="
122
122
  - !ruby/object:Gem::Version
123
123
  version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: lowtype
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '1.7'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '1.7'
124
138
  - !ruby/object:Gem::Dependency
125
139
  name: plugs
126
140
  requirement: !ruby/object:Gem::Requirement
@@ -211,9 +225,13 @@ files:
211
225
  - lib/raindown/config.rb
212
226
  - lib/raindown/directives.rb
213
227
  - lib/raindown/document.rb
228
+ - lib/raindown/editing.rb
214
229
  - lib/raindown/endpoint/curl.rb
230
+ - lib/raindown/endpoint/curl_response.rb
231
+ - lib/raindown/endpoint/endpoint_args.rb
215
232
  - lib/raindown/endpoint/endpoint_runner.rb
216
233
  - lib/raindown/endpoint/endpoint_variables.rb
234
+ - lib/raindown/endpoint/schema_block.rb
217
235
  - lib/raindown/files.rb
218
236
  - lib/raindown/fragments.rb
219
237
  - lib/raindown/front_matter.rb
@@ -221,6 +239,7 @@ files:
221
239
  - lib/raindown/heading_ids.rb
222
240
  - lib/raindown/js/host.mjs
223
241
  - lib/raindown/js/js_nodes.rb
242
+ - lib/raindown/markdown_nodes.rb
224
243
  - lib/raindown/node.rb
225
244
  - lib/raindown/node_blocks.rb
226
245
  - lib/raindown/node_injector.rb
@@ -230,6 +249,7 @@ files:
230
249
  - lib/raindown/pre_renderer.rb
231
250
  - lib/raindown/projects.rb
232
251
  - lib/raindown/rewriter.rb
252
+ - lib/raindown/server.rb
233
253
  - lib/raindown/stat.rb
234
254
  - lib/raindown/table_formatter.rb
235
255
  - lib/raindown/toc.rb
@@ -305,6 +325,7 @@ files:
305
325
  - lib/raindown/watcher/file_watcher.rb
306
326
  - lib/raindown/watcher/project.rb
307
327
  - lib/raindown/watcher/supervisor.rb
328
+ - lib/raindown/wiki_links.rb
308
329
  - lib/raindown/workers/worker.rb
309
330
  - lib/raindown/workers/workers.rb
310
331
  - lib/version.rb