raindown 0.3.0 → 0.5.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.
- checksums.yaml +4 -4
- data/lib/raindown/actions.rb +54 -0
- data/lib/raindown/cli.rb +99 -15
- data/lib/raindown/directives.rb +2 -2
- data/lib/raindown/document.rb +148 -0
- data/lib/raindown/endpoint/endpoint_runner.rb +15 -5
- data/lib/raindown/node_blocks.rb +29 -18
- 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/toc.rb +10 -5
- 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 +16 -18
- data/lib/raindown.rb +0 -3
- data/lib/version.rb +1 -1
- metadata +36 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae1eb87d8271ddcfcc3fb956311e11229421188a33f5bd7e0af70cffd2f49719
|
|
4
|
+
data.tar.gz: 380a98d835cfa9de899c5870828ba157e89aa0d3a4b8621a495f1a0ecd1e9bf6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3e74f64840850c572446865664e5368915df5065f09e9483d530d58392acb13f0e20ce872b9f8ee1930154af82d4c3baf8bf6fbd7c370399315e3b7a3a3b480
|
|
7
|
+
data.tar.gz: '09a95ee90ef2568aff8c783ecee3b2ac06590f0a71f1bde6e14e99e7e8f5c6e05776ca6ff3c7dcce22fd55a56e456273ecb0de2475fceb0785a312a022cd48fa'
|
|
@@ -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.file?(path), 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
|
data/lib/raindown/cli.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'fileutils'
|
|
3
4
|
require 'trees'
|
|
4
5
|
require_relative '../raindown'
|
|
6
|
+
require_relative '../version'
|
|
5
7
|
require_relative 'rewriter'
|
|
8
|
+
require_relative 'supervisor'
|
|
6
9
|
require_relative 'watcher'
|
|
7
10
|
|
|
8
11
|
module Raindown
|
|
@@ -10,22 +13,44 @@ module Raindown
|
|
|
10
13
|
extend Trees
|
|
11
14
|
|
|
12
15
|
USAGE = <<~TEXT
|
|
13
|
-
Usage: raindown
|
|
16
|
+
Usage: raindown [PATH] [options]
|
|
17
|
+
raindown FILE --html [OUTPUT]
|
|
18
|
+
raindown init [DIRECTORY]
|
|
19
|
+
raindown tui [PATH]
|
|
14
20
|
|
|
15
|
-
|
|
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.
|
|
23
|
+
|
|
24
|
+
Commands:
|
|
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
|
|
16
28
|
|
|
17
29
|
Options:
|
|
18
|
-
|
|
19
|
-
--
|
|
20
|
-
|
|
21
|
-
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
|
|
22
33
|
--bare Allow bare directives like :toc in Markdown files
|
|
23
34
|
-x, --execute Run endpoint curl snippets that have no output yet
|
|
24
35
|
-h, --help Show this message
|
|
36
|
+
-v, --version Show the version
|
|
37
|
+
|
|
38
|
+
Set RAINDOWN_DEBUG=1 to show where an error happened.
|
|
25
39
|
TEXT
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
CONFIG = <<~YAML
|
|
42
|
+
# Raindown project settings. See https://raindown.dev/docs/config
|
|
43
|
+
#
|
|
44
|
+
# Show directives as HTML comments rather than in bold:
|
|
45
|
+
# hide_directives: true
|
|
46
|
+
#
|
|
47
|
+
# Leave code blocks unhighlighted, so the browser can highlight them instead:
|
|
48
|
+
# highlight_code: false
|
|
49
|
+
YAML
|
|
50
|
+
|
|
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
|
|
29
54
|
|
|
30
55
|
# Trees matches tokens in order, so there's a line for every order and spelling of the options.
|
|
31
56
|
OPTION_LINES = (0..OPTIONS.size).flat_map { |count| OPTIONS.keys.permutation(count).to_a }.flat_map do |names|
|
|
@@ -35,26 +60,82 @@ module Raindown
|
|
|
35
60
|
OPTION_LINES.each do |names, flags|
|
|
36
61
|
# Endpoints run while formatting, which --stdin skips.
|
|
37
62
|
next if names.include?(:stdin) && names.include?(:execute)
|
|
63
|
+
next if names.include?(:html) && names.include?(:html_to)
|
|
38
64
|
|
|
39
65
|
command = if names.include?(:stdin) then :render_stdin
|
|
40
|
-
elsif names.
|
|
66
|
+
elsif names.intersect?(%i[html html_to]) then :render
|
|
41
67
|
else :watch
|
|
42
68
|
end
|
|
43
69
|
options = { bare: names.include?(:bare), execute: names.include?(:execute) }
|
|
44
70
|
pattern = [':path', *flags].join(' ')
|
|
45
71
|
|
|
46
|
-
if names.include?(:
|
|
72
|
+
if names.include?(:html_to)
|
|
47
73
|
line(pattern) { |path, output| execute { send(command, path, output, **options) } }
|
|
48
74
|
else
|
|
49
75
|
line(pattern) { |path| execute { send(command, path, **options) } }
|
|
50
76
|
end
|
|
51
77
|
end
|
|
52
78
|
|
|
53
|
-
# The :path line would match a help flag as the file, so help is checked first.
|
|
54
79
|
def self.run(argv)
|
|
55
|
-
|
|
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)
|
|
56
85
|
|
|
57
86
|
super || abort("Unknown arguments: #{argv.join(' ')}\n\n#{USAGE}")
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
report(e, path: argv.first)
|
|
89
|
+
end
|
|
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
|
+
|
|
105
|
+
# Shows an error in a file as one line, rather than a backtrace, unless RAINDOWN_DEBUG is set.
|
|
106
|
+
def self.report(error, path:)
|
|
107
|
+
raise error if ENV['RAINDOWN_DEBUG']
|
|
108
|
+
|
|
109
|
+
abort("raindown: #{path}: #{error.message}")
|
|
110
|
+
end
|
|
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
|
+
|
|
121
|
+
# Creates the .raindown folder that marks a project's root and holds its config.yml and nodes.
|
|
122
|
+
def self.init(directory = nil)
|
|
123
|
+
folder = File.join(directory || Dir.pwd, Nodes::FOLDER)
|
|
124
|
+
config = File.join(folder, Config::FILE)
|
|
125
|
+
return puts("#{config} already exists") if File.exist?(config)
|
|
126
|
+
|
|
127
|
+
FileUtils.mkdir_p(folder)
|
|
128
|
+
File.write(config, CONFIG)
|
|
129
|
+
puts "Created #{config}"
|
|
130
|
+
puts 'Put your own nodes in the same folder. See https://raindown.dev/docs/nodes'
|
|
131
|
+
end
|
|
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)
|
|
58
139
|
end
|
|
59
140
|
|
|
60
141
|
class << self
|
|
@@ -62,6 +143,7 @@ module Raindown
|
|
|
62
143
|
|
|
63
144
|
def render(path, output = nil, bare: false, execute: false)
|
|
64
145
|
ensure_exists(path)
|
|
146
|
+
abort('--html renders a file, not a directory') if File.directory?(path)
|
|
65
147
|
Nodes.load(path)
|
|
66
148
|
|
|
67
149
|
metadata = Pages.for(path)
|
|
@@ -75,11 +157,13 @@ module Raindown
|
|
|
75
157
|
**Config.render_options(path)))
|
|
76
158
|
end
|
|
77
159
|
|
|
78
|
-
|
|
160
|
+
# Watching only writes back to the files, while --html and --stdin render HTML.
|
|
161
|
+
def watch(path, bare: false, execute: false)
|
|
79
162
|
ensure_exists(path)
|
|
80
|
-
|
|
163
|
+
return Supervisor.new(directory: path, bare:, execute:).start if File.directory?(path)
|
|
81
164
|
|
|
82
|
-
|
|
165
|
+
Nodes.load(path)
|
|
166
|
+
Watcher.new(path:, bare:, execute:).start
|
|
83
167
|
end
|
|
84
168
|
|
|
85
169
|
def write(output, html)
|
data/lib/raindown/directives.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'cgi/escape'
|
|
4
4
|
|
|
5
5
|
module Raindown
|
|
6
6
|
# Content directives, like a node's directive and end tag or an endpoint's, are kept in the HTML:
|
|
@@ -22,7 +22,7 @@ module Raindown
|
|
|
22
22
|
def render(html, hide: false)
|
|
23
23
|
html.gsub(MARKER) do
|
|
24
24
|
text = ::Regexp.last_match(1)
|
|
25
|
-
hide ? "<!-- #{text} -->" : "<p><strong>#{
|
|
25
|
+
hide ? "<!-- #{text} -->" : "<p><strong>#{CGI.escapeHTML(text.to_s)}</strong></p>"
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
end
|
|
@@ -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. A project's own nodes are only directives once they're loaded,
|
|
36
|
+
# 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
|
|
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
|
|
@@ -28,10 +29,11 @@ module Raindown
|
|
|
28
29
|
new(content:, bare: BareDirectives.enabled?(path:, 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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'lowkey'
|
|
4
|
+
|
|
3
5
|
require_relative 'code_blocks'
|
|
4
6
|
require_relative 'directives'
|
|
5
7
|
require_relative 'front_matter'
|
|
@@ -36,21 +38,31 @@ module Raindown
|
|
|
36
38
|
# Each block outside code and front matter as { open:, close:, last:, node: }, where close is nil for a bare block.
|
|
37
39
|
# A self-closing node is a block of one line with self_closing: true.
|
|
38
40
|
def find(lines, front_matter: {}, bare: false)
|
|
39
|
-
|
|
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 = []
|
|
40
52
|
index = FrontMatter.line_count(lines)
|
|
41
53
|
|
|
42
54
|
while index < lines.length
|
|
43
55
|
if (code = CodeBlocks.at(lines, index))
|
|
44
56
|
index = code[:close]
|
|
45
|
-
elsif (
|
|
46
|
-
|
|
47
|
-
index =
|
|
57
|
+
elsif (span = span_at(lines, index, bare:))
|
|
58
|
+
spans << span
|
|
59
|
+
index = span[:last]
|
|
48
60
|
end
|
|
49
61
|
|
|
50
62
|
index += 1
|
|
51
63
|
end
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
spans
|
|
54
66
|
end
|
|
55
67
|
|
|
56
68
|
# Swaps each block for its node's HTML, through a placeholder, or for its Markdown when it has no HTML of its own.
|
|
@@ -78,15 +90,15 @@ module Raindown
|
|
|
78
90
|
"#{Directives.marker(line)}\n"
|
|
79
91
|
end
|
|
80
92
|
|
|
81
|
-
def
|
|
93
|
+
def span_at(lines, index, bare:)
|
|
82
94
|
found, pattern = directive(lines[index], bare:)
|
|
83
95
|
return unless found && (klass = Nodes.node_class(found[2]))
|
|
84
96
|
|
|
85
|
-
|
|
97
|
+
span = { name: found[2], class: klass, args: found[3].to_s }
|
|
86
98
|
case pattern
|
|
87
|
-
when :open then end_tag(lines, index, found[2]).then { |close| { open: index, close:, last: close,
|
|
88
|
-
when :bare_open then bare_block(lines, index,
|
|
89
|
-
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 }
|
|
90
102
|
end
|
|
91
103
|
end
|
|
92
104
|
|
|
@@ -104,11 +116,11 @@ module Raindown
|
|
|
104
116
|
end
|
|
105
117
|
|
|
106
118
|
# A bare block holds the lines straight below it, up to the next blank line, with fenced code kept whole.
|
|
107
|
-
def bare_block(lines, open
|
|
119
|
+
def bare_block(lines, open)
|
|
108
120
|
last = open
|
|
109
121
|
last = CodeBlocks.at(lines, last + 1)&.fetch(:close) || (last + 1) while lines[last + 1] && !lines[last + 1].strip.empty?
|
|
110
122
|
|
|
111
|
-
{ open:, close: nil, last
|
|
123
|
+
{ open:, close: nil, last: }
|
|
112
124
|
end
|
|
113
125
|
|
|
114
126
|
def end_tag(lines, open, name)
|
|
@@ -131,13 +143,12 @@ module Raindown
|
|
|
131
143
|
text.scan(PROP).to_h { |name, value| [name.to_sym, value(value, variables)] }
|
|
132
144
|
end
|
|
133
145
|
|
|
146
|
+
# A value, like 'text', 3, true, nil or :draft, or else a variable, like author.name.
|
|
134
147
|
def value(value, variables)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
when 'true', 'false' then value == 'true'
|
|
140
|
-
else Variables.value(variables, value)
|
|
148
|
+
Lowkey::ValueParser.parse(value) do
|
|
149
|
+
next value[1..-2] if value.match?(/\A(["']).*\1\z/)
|
|
150
|
+
|
|
151
|
+
Variables.value(variables, value)
|
|
141
152
|
end
|
|
142
153
|
end
|
|
143
154
|
end
|
|
@@ -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
|