raindown 0.4.0 → 0.5.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/actions.rb +54 -0
- data/lib/raindown/bare_directives.rb +14 -3
- data/lib/raindown/cli.rb +59 -17
- data/lib/raindown/document.rb +148 -0
- data/lib/raindown/endpoint/endpoint_runner.rb +16 -6
- data/lib/raindown/node_blocks.rb +22 -12
- data/lib/raindown/node_injector.rb +1 -1
- data/lib/raindown/project_worker.rb +48 -0
- data/lib/raindown/projects.rb +71 -0
- data/lib/raindown/supervisor.rb +160 -0
- data/lib/raindown/tui/commands.rb +70 -0
- data/lib/raindown/tui/file_tree.rb +55 -0
- data/lib/raindown/tui/input.rb +55 -0
- data/lib/raindown/tui/layout.rb +142 -0
- data/lib/raindown/tui/navigator.rb +123 -0
- data/lib/raindown/tui/outline.rb +114 -0
- data/lib/raindown/tui/pane.rb +41 -0
- data/lib/raindown/tui/row.rb +16 -0
- data/lib/raindown/tui/screen.rb +131 -0
- data/lib/raindown/tui/styles.rb +36 -0
- data/lib/raindown/tui.rb +27 -0
- data/lib/raindown/watcher.rb +7 -23
- data/lib/raindown.rb +2 -0
- data/lib/version.rb +1 -1
- metadata +32 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e35dfde9371c5f805bff304124c0abe22ca89254f14c5c70d2996e14d0dbb0f
|
|
4
|
+
data.tar.gz: 76774aa6b06fec8b50e0929b9ac86259511fb1ab9417ce48fcfa56afd20c90cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 454c35bfd6c74cc2c125d8229ffe971cb178fb40f4d87affad0fc5d345301fd4d5a0e32a15852f05eb35231e0054b2a5b55ff2a4eb7253cc85aef17f4d17e178
|
|
7
|
+
data.tar.gz: 33b41ed491f7cc8238f3c3d99b2d72e2b0197dce29146dd14bdc704dd0ea6fa64788c97ed5d60bda60ef4602e9e6dedb2d22b02a1f8886a5c4592a522d96886e
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
require_relative '../raindown'
|
|
6
|
+
require_relative 'bare_directives'
|
|
7
|
+
require_relative 'endpoint/endpoint_runner'
|
|
8
|
+
require_relative 'rewriter'
|
|
9
|
+
|
|
10
|
+
module Raindown
|
|
11
|
+
# What a tool can do to a file from its Document, like the terminal UI, without knowing how it's drawn.
|
|
12
|
+
# Each returns true when the file changed, and raises when it can't, for the tool to show.
|
|
13
|
+
module Actions
|
|
14
|
+
class NotAnEndpoint < StandardError; end
|
|
15
|
+
|
|
16
|
+
# Editors that open a file at a line with "path:line" rather than "+line path".
|
|
17
|
+
PATH_LINE_EDITORS = %w[hx helix subl zed].freeze
|
|
18
|
+
# Editors that need -g before "path:line", and --wait so the tool waits for the file to close.
|
|
19
|
+
GOTO_EDITORS = %w[code codium cursor windsurf].freeze
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# Writes back the whole file, like saving it while it's watched.
|
|
24
|
+
def write_back(path)
|
|
25
|
+
Rewriter.update(path:, metadata: Pages.for(path))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Runs one endpoint's request and writes its response, replacing any response it had.
|
|
29
|
+
def execute(path, directive)
|
|
30
|
+
raise NotAnEndpoint, "#{directive.name} on line #{directive.line} isn't an endpoint" unless directive.endpoint?
|
|
31
|
+
|
|
32
|
+
content = File.read(path, encoding: Encoding::UTF_8)
|
|
33
|
+
rewritten = EndpointRunner.new(content:, bare: BareDirectives.enabled?(path:, content:), only: directive.line - 1).rewrite
|
|
34
|
+
return false if rewritten == content
|
|
35
|
+
|
|
36
|
+
File.write(path, rewritten)
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The command that opens the file at a line in the person's editor, like ["vim", "+12", "notes.md"].
|
|
41
|
+
def edit_command(path, line:, editor: ENV['VISUAL'] || ENV['EDITOR'] || 'vi')
|
|
42
|
+
command = Shellwords.split(editor)
|
|
43
|
+
name = File.basename(command.first)
|
|
44
|
+
|
|
45
|
+
if GOTO_EDITORS.include?(name)
|
|
46
|
+
[*command, *('--wait' unless command.include?('--wait')), '-g', "#{path}:#{line}"]
|
|
47
|
+
elsif PATH_LINE_EDITORS.include?(name)
|
|
48
|
+
[*command, "#{path}:#{line}"]
|
|
49
|
+
else
|
|
50
|
+
[*command, "+#{line}", path]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -5,7 +5,8 @@ require_relative 'front_matter'
|
|
|
5
5
|
|
|
6
6
|
module Raindown
|
|
7
7
|
# Raindown files (.rd/.raindown) can write a directive like <{ if: draft }> as a bare if: draft on its own line.
|
|
8
|
-
# Markdown files opt in with
|
|
8
|
+
# Markdown files opt in with "raindown: true" in their front matter, or with the --bare flag.
|
|
9
|
+
# Nodes like toc: and files: are handled by NodeBlocks, before the rest.
|
|
9
10
|
#
|
|
10
11
|
# Bare directives have no end tag. A bare block directive closes itself after the content it applies to,
|
|
11
12
|
# so where a block needs to wrap more than that, write it with delimiters and an end tag instead:
|
|
@@ -24,8 +25,8 @@ module Raindown
|
|
|
24
25
|
|
|
25
26
|
module_function
|
|
26
27
|
|
|
27
|
-
def enabled?(path: nil, bare: false)
|
|
28
|
-
bare || file?(path)
|
|
28
|
+
def enabled?(path: nil, content: nil, bare: false)
|
|
29
|
+
bare || file?(path) || front_matter?(content)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def file?(path)
|
|
@@ -34,6 +35,16 @@ module Raindown
|
|
|
34
35
|
EXTENSIONS.include?(File.extname(path).downcase)
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
# Whether the front matter has "raindown: true". Front matter that isn't valid YAML fails when the file renders,
|
|
39
|
+
# so here it only means no.
|
|
40
|
+
def front_matter?(content)
|
|
41
|
+
return false unless content&.start_with?('---')
|
|
42
|
+
|
|
43
|
+
FrontMatter.parse(content).first[:raindown] == true
|
|
44
|
+
rescue Psych::Exception
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
37
48
|
# Wrap each bare directive in Antlers delimiters and close its block, leaving code and front matter as-is.
|
|
38
49
|
def expand(content)
|
|
39
50
|
lines = content.lines
|
data/lib/raindown/cli.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'trees'
|
|
|
5
5
|
require_relative '../raindown'
|
|
6
6
|
require_relative '../version'
|
|
7
7
|
require_relative 'rewriter'
|
|
8
|
+
require_relative 'supervisor'
|
|
8
9
|
require_relative 'watcher'
|
|
9
10
|
|
|
10
11
|
module Raindown
|
|
@@ -12,19 +13,23 @@ module Raindown
|
|
|
12
13
|
extend Trees
|
|
13
14
|
|
|
14
15
|
USAGE = <<~TEXT
|
|
15
|
-
Usage: raindown
|
|
16
|
+
Usage: raindown [PATH] [options]
|
|
17
|
+
raindown FILE --html [OUTPUT]
|
|
16
18
|
raindown init [DIRECTORY]
|
|
19
|
+
raindown tui [PATH]
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
Watches PATH and writes back to its Markdown files as they're saved, like their tables of contents and tables.
|
|
22
|
+
PATH is a file, or a directory (default: current directory) whose projects with a .raindown folder are watched.
|
|
19
23
|
|
|
20
24
|
Commands:
|
|
21
25
|
init [DIRECTORY] Create a .raindown folder for the project's config and nodes (default: current directory)
|
|
26
|
+
tui [PATH] Browse a project's files and outlines in the terminal, fold directives, run endpoints
|
|
27
|
+
and open the editor at a line
|
|
22
28
|
|
|
23
29
|
Options:
|
|
24
|
-
|
|
25
|
-
--
|
|
26
|
-
|
|
27
|
-
its extension (to allow bare directives) and can be unsaved
|
|
30
|
+
--html [OUTPUT] Render FILE to HTML once, writing to OUTPUT or else printing to stdout
|
|
31
|
+
--stdin Render Markdown from stdin to HTML once, without formatting FILE, which only needs
|
|
32
|
+
its extension (to allow bare directives) and can be unsaved. Add --html OUTPUT to write a file
|
|
28
33
|
--bare Allow bare directives like :toc in Markdown files
|
|
29
34
|
-x, --execute Run endpoint curl snippets that have no output yet
|
|
30
35
|
-h, --help Show this message
|
|
@@ -43,8 +48,9 @@ module Raindown
|
|
|
43
48
|
# highlight_code: false
|
|
44
49
|
YAML
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
# "--html OUTPUT" is matched as "--html-to OUTPUT" (see run), so a flag after a bare --html isn't read as the OUTPUT.
|
|
52
|
+
OPTIONS = { html_to: ['--html-to :output'], html: ['--html'], bare: ['--bare'], execute: ['-x', '--execute'],
|
|
53
|
+
stdin: ['--stdin'] }.freeze
|
|
48
54
|
|
|
49
55
|
# Trees matches tokens in order, so there's a line for every order and spelling of the options.
|
|
50
56
|
OPTION_LINES = (0..OPTIONS.size).flat_map { |count| OPTIONS.keys.permutation(count).to_a }.flat_map do |names|
|
|
@@ -54,32 +60,48 @@ module Raindown
|
|
|
54
60
|
OPTION_LINES.each do |names, flags|
|
|
55
61
|
# Endpoints run while formatting, which --stdin skips.
|
|
56
62
|
next if names.include?(:stdin) && names.include?(:execute)
|
|
63
|
+
next if names.include?(:html) && names.include?(:html_to)
|
|
57
64
|
|
|
58
65
|
command = if names.include?(:stdin) then :render_stdin
|
|
59
|
-
elsif names.
|
|
66
|
+
elsif names.intersect?(%i[html html_to]) then :render
|
|
60
67
|
else :watch
|
|
61
68
|
end
|
|
62
69
|
options = { bare: names.include?(:bare), execute: names.include?(:execute) }
|
|
63
70
|
pattern = [':path', *flags].join(' ')
|
|
64
71
|
|
|
65
|
-
if names.include?(:
|
|
72
|
+
if names.include?(:html_to)
|
|
66
73
|
line(pattern) { |path, output| execute { send(command, path, output, **options) } }
|
|
67
74
|
else
|
|
68
75
|
line(pattern) { |path| execute { send(command, path, **options) } }
|
|
69
76
|
end
|
|
70
77
|
end
|
|
71
78
|
|
|
72
|
-
# The :path line would match help, version and init as the file, so they're checked first.
|
|
73
79
|
def self.run(argv)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
return if command(argv)
|
|
81
|
+
|
|
82
|
+
# Without a path, like "raindown" or "raindown -x", the current directory is watched.
|
|
83
|
+
argv = ['.', *argv] if argv.empty? || argv.first.start_with?('-')
|
|
84
|
+
argv = html_output(argv)
|
|
77
85
|
|
|
78
86
|
super || abort("Unknown arguments: #{argv.join(' ')}\n\n#{USAGE}")
|
|
79
87
|
rescue StandardError => e
|
|
80
88
|
report(e, path: argv.first)
|
|
81
89
|
end
|
|
82
90
|
|
|
91
|
+
# The :path line would match help, version, init and tui as the file, so they're checked first.
|
|
92
|
+
# Returns true when argv was one of them.
|
|
93
|
+
def self.command(argv)
|
|
94
|
+
abort(USAGE) if argv.intersect?(%w[-h --help])
|
|
95
|
+
|
|
96
|
+
if argv.intersect?(%w[-v --version]) then puts(VERSION)
|
|
97
|
+
elsif argv.first == 'init' && argv.size <= 2 then init(argv[1])
|
|
98
|
+
elsif argv.first == 'tui' && argv.size <= 2 then tui(argv[1] || '.')
|
|
99
|
+
else return false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
|
|
83
105
|
# Shows an error in a file as one line, rather than a backtrace, unless RAINDOWN_DEBUG is set.
|
|
84
106
|
def self.report(error, path:)
|
|
85
107
|
raise error if ENV['RAINDOWN_DEBUG']
|
|
@@ -87,6 +109,15 @@ module Raindown
|
|
|
87
109
|
abort("raindown: #{path}: #{error.message}")
|
|
88
110
|
end
|
|
89
111
|
|
|
112
|
+
# "--html out.html" names an OUTPUT, while "--html --bare" doesn't, as OUTPUT never starts with "-".
|
|
113
|
+
def self.html_output(argv)
|
|
114
|
+
index = argv.index('--html') || (return argv)
|
|
115
|
+
output = argv[index + 1]
|
|
116
|
+
return argv if output.nil? || output.start_with?('-')
|
|
117
|
+
|
|
118
|
+
[*argv[0...index], '--html-to', *argv[(index + 1)..]]
|
|
119
|
+
end
|
|
120
|
+
|
|
90
121
|
# Creates the .raindown folder that marks a project's root and holds its config.yml and nodes.
|
|
91
122
|
def self.init(directory = nil)
|
|
92
123
|
folder = File.join(directory || Dir.pwd, Nodes::FOLDER)
|
|
@@ -99,11 +130,20 @@ module Raindown
|
|
|
99
130
|
puts 'Put your own nodes in the same folder. See https://raindown.dev/docs/nodes'
|
|
100
131
|
end
|
|
101
132
|
|
|
133
|
+
# The terminal UI loads curses, so it's only required when it starts.
|
|
134
|
+
def self.tui(path)
|
|
135
|
+
abort("File not found: #{path}") unless File.exist?(path)
|
|
136
|
+
|
|
137
|
+
require_relative 'tui'
|
|
138
|
+
TUI.start(path)
|
|
139
|
+
end
|
|
140
|
+
|
|
102
141
|
class << self
|
|
103
142
|
private
|
|
104
143
|
|
|
105
144
|
def render(path, output = nil, bare: false, execute: false)
|
|
106
145
|
ensure_exists(path)
|
|
146
|
+
abort('--html renders a file, not a directory') if File.directory?(path)
|
|
107
147
|
Nodes.load(path)
|
|
108
148
|
|
|
109
149
|
metadata = Pages.for(path)
|
|
@@ -117,11 +157,13 @@ module Raindown
|
|
|
117
157
|
**Config.render_options(path)))
|
|
118
158
|
end
|
|
119
159
|
|
|
120
|
-
|
|
160
|
+
# Watching only writes back to the files, while --html and --stdin render HTML.
|
|
161
|
+
def watch(path, bare: false, execute: false)
|
|
121
162
|
ensure_exists(path)
|
|
122
|
-
|
|
163
|
+
return Supervisor.new(directory: path, bare:, execute:).start if File.directory?(path)
|
|
123
164
|
|
|
124
|
-
|
|
165
|
+
Nodes.load(path)
|
|
166
|
+
Watcher.new(path:, bare:, execute:).start
|
|
125
167
|
end
|
|
126
168
|
|
|
127
169
|
def write(output, html)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'commonmarker'
|
|
4
|
+
|
|
5
|
+
require_relative 'bare_directives'
|
|
6
|
+
require_relative 'code_blocks'
|
|
7
|
+
require_relative 'endpoint/endpoint_runner'
|
|
8
|
+
require_relative 'front_matter'
|
|
9
|
+
require_relative 'node_blocks'
|
|
10
|
+
|
|
11
|
+
module Raindown
|
|
12
|
+
# A Markdown file's structure, read from its source so each part has the line it's on, for tools like an editor.
|
|
13
|
+
# Headings that a directive writes back below it are in the source too, while ones a node only renders as HTML aren't.
|
|
14
|
+
# Lines count from 1, from the top of the file.
|
|
15
|
+
class Document
|
|
16
|
+
Heading = Data.define(:level, :text, :line)
|
|
17
|
+
|
|
18
|
+
# A content directive: a node, like toc: or a project's Changelog:, or an endpoint.
|
|
19
|
+
# It spans line to end_line, its end tag, or the last line of a bare block.
|
|
20
|
+
# Output is the lines Raindown writes back, like a node's Markdown or an endpoint's response, and is nil
|
|
21
|
+
# when nothing's written yet. A self-closing directive like <{ :toc }> only renders, so it never writes.
|
|
22
|
+
Directive = Data.define(:name, :args, :line, :end_line, :output, :writes) do
|
|
23
|
+
def endpoint? = name == 'endpoint'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Inline nodes whose text a reader sees, like code spans, as opposed to inline HTML tags.
|
|
27
|
+
TEXT_NODES = %i[text code].freeze
|
|
28
|
+
BREAK_NODES = %i[softbreak linebreak].freeze
|
|
29
|
+
ENDPOINT_END_TAG = /\A {0,3}<\{\s*:endpoint\s*\}>[ \t]*$/
|
|
30
|
+
|
|
31
|
+
def self.parse(markdown, bare: false)
|
|
32
|
+
new(markdown:, bare:)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Raindown files read bare directives, as do Markdown files with "raindown: true" front matter.
|
|
36
|
+
# A project's own nodes are only directives once they're loaded, like with Nodes.load(path), the same as when the file renders.
|
|
37
|
+
def self.read(path)
|
|
38
|
+
new(markdown: File.read(path, encoding: Encoding::UTF_8), bare: BareDirectives.file?(path))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(markdown:, bare: false)
|
|
42
|
+
@markdown = markdown
|
|
43
|
+
@lines = markdown.lines
|
|
44
|
+
@bare = bare || BareDirectives.front_matter?(markdown)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The headings in the order they appear, with their source text, like "Notes for {name}" before it's rendered.
|
|
48
|
+
def headings
|
|
49
|
+
@headings ||= nodes(:heading).map do |node|
|
|
50
|
+
Heading.new(level: node.header_level, text: text(node), line: node.source_position[:start_line])
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The content directives in the order they appear. Template directives like if: and for: aren't here,
|
|
55
|
+
# since they only change what renders, and write nothing back.
|
|
56
|
+
def directives
|
|
57
|
+
@directives ||= (node_directives + endpoint_directives).sort_by(&:line)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The headings and directives together, in the order they appear.
|
|
61
|
+
def outline
|
|
62
|
+
(headings + directives).sort_by(&:line)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def root
|
|
68
|
+
@root ||= Commonmarker.parse(@markdown, options: MARKDOWN_OPTIONS)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def nodes(type)
|
|
72
|
+
[].tap { |nodes| root.walk { |node| nodes << node if node.type == type } }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def text(heading)
|
|
76
|
+
parts = []
|
|
77
|
+
heading.walk do |node|
|
|
78
|
+
parts << node.string_content if TEXT_NODES.include?(node.type)
|
|
79
|
+
parts << ' ' if BREAK_NODES.include?(node.type)
|
|
80
|
+
end
|
|
81
|
+
parts.join.strip
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def node_directives
|
|
85
|
+
NodeBlocks.spans(@lines, bare: @bare).map do |span|
|
|
86
|
+
output = trimmed(span[:open] + 1, span[:close] ? span[:close] - 1 : span[:last])
|
|
87
|
+
directive(span[:name], span[:args], span[:open]..span[:last], output:, writes: !span[:self_closing])
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def endpoint_directives
|
|
92
|
+
directives = []
|
|
93
|
+
index = FrontMatter.line_count(@lines)
|
|
94
|
+
|
|
95
|
+
while index < @lines.length
|
|
96
|
+
if (code = CodeBlocks.at(@lines, index))
|
|
97
|
+
index = code[:close]
|
|
98
|
+
elsif (args = endpoint_args(@lines[index]))
|
|
99
|
+
directives << endpoint(index, args)
|
|
100
|
+
index = directives.last.end_line - 1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
index += 1
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
directives
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def endpoint_args(line)
|
|
110
|
+
line = BareDirectives.wrap(line) || line if @bare
|
|
111
|
+
line[EndpointRunner::DIRECTIVE, 1]&.strip
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The response and the date it was fetched are the output, while the curl snippet above them is the author's.
|
|
115
|
+
def endpoint(open, args)
|
|
116
|
+
curl, response = CodeBlocks.endpoint(@lines, open + 1)
|
|
117
|
+
output = response && (response[:open]..(fetched?(response[:close] + 1) ? response[:close] + 1 : response[:close]))
|
|
118
|
+
last = output&.end || curl&.fetch(:close) || open
|
|
119
|
+
# A bare endpoint, which isn't delimited, ends after its blocks.
|
|
120
|
+
last = end_tag(after: last) || last if @lines[open].match?(EndpointRunner::DIRECTIVE)
|
|
121
|
+
|
|
122
|
+
directive('endpoint', args, open..last, output:, writes: true)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def fetched?(index)
|
|
126
|
+
@lines[index]&.match?(EndpointRunner::FETCHED)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The end tag after the endpoint's blocks, with only blank lines between.
|
|
130
|
+
def end_tag(after:)
|
|
131
|
+
index = CodeBlocks.skip_blank_lines(@lines, after + 1)
|
|
132
|
+
index if @lines[index]&.match?(ENDPOINT_END_TAG)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Lines from first to last with the blank lines around them left out, or nil when there are none.
|
|
136
|
+
def trimmed(first, last)
|
|
137
|
+
first += 1 while first <= last && @lines[first].strip.empty?
|
|
138
|
+
last -= 1 while last >= first && @lines[last].strip.empty?
|
|
139
|
+
first..last if first <= last
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# From a range of indexes to line numbers.
|
|
143
|
+
def directive(name, args, span, output:, writes:)
|
|
144
|
+
output = (output.begin + 1)..(output.end + 1) if output
|
|
145
|
+
Directive.new(name:, args:, line: span.begin + 1, end_line: span.end + 1, output:, writes:)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -9,6 +9,7 @@ module Raindown
|
|
|
9
9
|
# Runs the curl block under an endpoint directive and writes the response in the code block below it,
|
|
10
10
|
# followed by the date it was fetched.
|
|
11
11
|
# Only runs when that block is missing or empty, and the directive has :x/:execute or --execute is passed.
|
|
12
|
+
# Or with only: and a directive's line index, runs just that endpoint, replacing any response it has.
|
|
12
13
|
# <{ endpoint: :x }>
|
|
13
14
|
# ```sh
|
|
14
15
|
# curl https://api.example.com
|
|
@@ -25,13 +26,14 @@ module Raindown
|
|
|
25
26
|
FETCHED = /\A {0,3}<!-- raindown:fetched [^\n]*? -->[ \t]*$/
|
|
26
27
|
|
|
27
28
|
def self.rewrite(content:, path: nil, bare: false, execute: false, **)
|
|
28
|
-
new(content:, bare: BareDirectives.enabled?(path:, bare:), execute:).rewrite
|
|
29
|
+
new(content:, bare: BareDirectives.enabled?(path:, content:, bare:), execute:).rewrite
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
def initialize(content:, bare: false, execute: false)
|
|
32
|
+
def initialize(content:, bare: false, execute: false, only: nil)
|
|
32
33
|
@lines = content.lines
|
|
33
34
|
@bare = bare
|
|
34
35
|
@execute = execute
|
|
36
|
+
@only = only
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
def rewrite
|
|
@@ -41,7 +43,7 @@ module Raindown
|
|
|
41
43
|
if (block = CodeBlocks.at(@lines, index))
|
|
42
44
|
index = block[:close]
|
|
43
45
|
elsif (args = directive_args(@lines[index]))
|
|
44
|
-
index = fill(index + 1, args)
|
|
46
|
+
index = fill(index + 1, args, directive: index)
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
index += 1
|
|
@@ -58,15 +60,20 @@ module Raindown
|
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
# Returns the index of the last line it handled.
|
|
61
|
-
def fill(index, args)
|
|
63
|
+
def fill(index, args, directive:)
|
|
62
64
|
curl, output = CodeBlocks.endpoint(@lines, index)
|
|
63
65
|
return index - 1 unless curl&.dig(:body)&.lstrip&.start_with?('curl')
|
|
64
|
-
|
|
65
|
-
return curl[:close] if (output && !output[:body].strip.empty?) || !execute?(args)
|
|
66
|
+
return curl[:close] unless run?(args, output, directive)
|
|
66
67
|
|
|
67
68
|
write_output(curl:, output:)
|
|
68
69
|
end
|
|
69
70
|
|
|
71
|
+
def run?(args, output, directive)
|
|
72
|
+
return directive == @only if @only
|
|
73
|
+
|
|
74
|
+
(output.nil? || output[:body].strip.empty?) && execute?(args)
|
|
75
|
+
end
|
|
76
|
+
|
|
70
77
|
def execute?(args)
|
|
71
78
|
@execute || args.intersect?(EXECUTE_ARGS)
|
|
72
79
|
end
|
|
@@ -80,6 +87,9 @@ module Raindown
|
|
|
80
87
|
@lines[start, output ? replaced_length(output) : 0] = block
|
|
81
88
|
start + block.length - 1
|
|
82
89
|
rescue Curl::Error => e
|
|
90
|
+
# The one endpoint that was asked for fails for the caller to show, like in the terminal UI.
|
|
91
|
+
raise if @only
|
|
92
|
+
|
|
83
93
|
warn "Endpoint failed: #{e.message}"
|
|
84
94
|
curl[:close]
|
|
85
95
|
end
|
data/lib/raindown/node_blocks.rb
CHANGED
|
@@ -38,21 +38,31 @@ module Raindown
|
|
|
38
38
|
# Each block outside code and front matter as { open:, close:, last:, node: }, where close is nil for a bare block.
|
|
39
39
|
# A self-closing node is a block of one line with self_closing: true.
|
|
40
40
|
def find(lines, front_matter: {}, bare: false)
|
|
41
|
-
|
|
41
|
+
variables = Variables.new(front_matter)
|
|
42
|
+
|
|
43
|
+
spans(lines, bare:).map do |span|
|
|
44
|
+
span.except(:name, :class, :args).merge(node: Nodes.instance(span[:class], *args(span[:args], variables)))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Each block like find's, with the node's name, class and props text in place of the node,
|
|
49
|
+
# so a block can be found without making its node, like for a document's outline.
|
|
50
|
+
def spans(lines, bare: false)
|
|
51
|
+
spans = []
|
|
42
52
|
index = FrontMatter.line_count(lines)
|
|
43
53
|
|
|
44
54
|
while index < lines.length
|
|
45
55
|
if (code = CodeBlocks.at(lines, index))
|
|
46
56
|
index = code[:close]
|
|
47
|
-
elsif (
|
|
48
|
-
|
|
49
|
-
index =
|
|
57
|
+
elsif (span = span_at(lines, index, bare:))
|
|
58
|
+
spans << span
|
|
59
|
+
index = span[:last]
|
|
50
60
|
end
|
|
51
61
|
|
|
52
62
|
index += 1
|
|
53
63
|
end
|
|
54
64
|
|
|
55
|
-
|
|
65
|
+
spans
|
|
56
66
|
end
|
|
57
67
|
|
|
58
68
|
# Swaps each block for its node's HTML, through a placeholder, or for its Markdown when it has no HTML of its own.
|
|
@@ -80,15 +90,15 @@ module Raindown
|
|
|
80
90
|
"#{Directives.marker(line)}\n"
|
|
81
91
|
end
|
|
82
92
|
|
|
83
|
-
def
|
|
93
|
+
def span_at(lines, index, bare:)
|
|
84
94
|
found, pattern = directive(lines[index], bare:)
|
|
85
95
|
return unless found && (klass = Nodes.node_class(found[2]))
|
|
86
96
|
|
|
87
|
-
|
|
97
|
+
span = { name: found[2], class: klass, args: found[3].to_s }
|
|
88
98
|
case pattern
|
|
89
|
-
when :open then end_tag(lines, index, found[2]).then { |close| { open: index, close:, last: close,
|
|
90
|
-
when :bare_open then bare_block(lines, index,
|
|
91
|
-
else { open: index, close: nil, last: index,
|
|
99
|
+
when :open then end_tag(lines, index, found[2]).then { |close| { open: index, close:, last: close, **span } }
|
|
100
|
+
when :bare_open then { **bare_block(lines, index), **span }
|
|
101
|
+
else { open: index, close: nil, last: index, self_closing: true, **span }
|
|
92
102
|
end
|
|
93
103
|
end
|
|
94
104
|
|
|
@@ -106,11 +116,11 @@ module Raindown
|
|
|
106
116
|
end
|
|
107
117
|
|
|
108
118
|
# A bare block holds the lines straight below it, up to the next blank line, with fenced code kept whole.
|
|
109
|
-
def bare_block(lines, open
|
|
119
|
+
def bare_block(lines, open)
|
|
110
120
|
last = open
|
|
111
121
|
last = CodeBlocks.at(lines, last + 1)&.fetch(:close) || (last + 1) while lines[last + 1] && !lines[last + 1].strip.empty?
|
|
112
122
|
|
|
113
|
-
{ open:, close: nil, last
|
|
123
|
+
{ open:, close: nil, last: }
|
|
114
124
|
end
|
|
115
125
|
|
|
116
126
|
def end_tag(lines, open, name)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../raindown'
|
|
4
|
+
require_relative 'rewriter'
|
|
5
|
+
|
|
6
|
+
module Raindown
|
|
7
|
+
# Writes back to one project's Markdown files as they're saved, like its TOCs, tables and endpoint responses,
|
|
8
|
+
# with the project's own nodes. Each line it shows starts with the project's name.
|
|
9
|
+
class ProjectWorker
|
|
10
|
+
attr_reader :root, :name
|
|
11
|
+
|
|
12
|
+
def initialize(root:, name: File.basename(root), bare: false, execute: false)
|
|
13
|
+
@root = root
|
|
14
|
+
@name = name
|
|
15
|
+
@bare = bare
|
|
16
|
+
@execute = execute
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Loads the project's nodes from its .raindown folder.
|
|
20
|
+
def start
|
|
21
|
+
Nodes.load(File.join(root, Nodes::FOLDER))
|
|
22
|
+
rescue StandardError, ScriptError => e
|
|
23
|
+
raise if ENV['RAINDOWN_DEBUG']
|
|
24
|
+
|
|
25
|
+
warn "[#{name}] raindown: #{Nodes::FOLDER}: #{e.message}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Writing back saves the file again, and that save changes nothing, so it ends there.
|
|
29
|
+
def update(path)
|
|
30
|
+
changed = Rewriter.update(path:, bare: @bare, execute: @execute, metadata: Pages.for(path))
|
|
31
|
+
puts "[#{name}] Updated #{relative(path)}" if changed
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
raise if ENV['RAINDOWN_DEBUG']
|
|
34
|
+
|
|
35
|
+
# An error in the file is shown rather than stopping the watcher, so it can be fixed and saved again.
|
|
36
|
+
warn "[#{name}] raindown: #{relative(path)}: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Workers in this process have nothing to stop, unlike a ForkedWorker's process.
|
|
40
|
+
def stop; end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def relative(path)
|
|
45
|
+
File.expand_path(path).delete_prefix(File.join(root, ''))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|