raindown 0.12.0 → 0.14.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/elements.rb +12 -25
- data/lib/lexemes/node_lexeme.rb +37 -0
- data/lib/nodes/list_node.rb +22 -25
- data/lib/nodes/write_in_node.rb +95 -0
- data/lib/raindown/bare_directives.rb +11 -17
- data/lib/raindown/chart/chart.rb +1 -1
- data/lib/raindown/code_blocks.rb +23 -15
- data/lib/raindown/context.rb +19 -0
- data/lib/raindown/directives.rb +1 -1
- data/lib/raindown/document.rb +9 -9
- data/lib/raindown/endpoint/endpoint_runner.rb +15 -24
- data/lib/raindown/fragments.rb +6 -1
- data/lib/raindown/js/host.mjs +2 -2
- data/lib/raindown/js/js_nodes.rb +4 -4
- data/lib/raindown/list.rb +31 -0
- data/lib/raindown/markdown_nodes.rb +3 -2
- data/lib/raindown/node.rb +10 -2
- data/lib/raindown/node_blocks.rb +28 -27
- data/lib/raindown/node_injector.rb +7 -1
- data/lib/raindown/nodes.rb +61 -21
- data/lib/raindown/pages.rb +14 -8
- data/lib/raindown/pre_renderer.rb +13 -10
- data/lib/raindown/stat.rb +1 -1
- data/lib/raindown/table/markdown_table.rb +35 -0
- data/lib/raindown/table/table.rb +87 -0
- data/lib/raindown/toc.rb +2 -2
- data/lib/raindown/tui/cloud.rb +154 -0
- data/lib/raindown/tui/feelings/celebration/celebration_renderer.rb +22 -0
- data/lib/raindown/tui/feelings/feelings_renderer.rb +3 -16
- data/lib/raindown/tui/layout.rb +8 -1
- data/lib/raindown/tui/project.rb +23 -11
- data/lib/raindown/tui/renderers/endpoint_renderer.rb +4 -3
- data/lib/raindown/tui/terminal/loading.rb +34 -0
- data/lib/raindown/tui/terminal/screen.rb +6 -5
- data/lib/raindown/tui/terminal/styles.rb +13 -1
- data/lib/raindown/tui/views/endpoint_view.rb +5 -4
- data/lib/raindown/tui.rb +10 -6
- data/lib/raindown/wiki_links.rb +1 -1
- data/lib/raindown.rb +9 -14
- data/lib/version.rb +1 -1
- metadata +11 -5
- data/lib/lexemes/list_lexeme.rb +0 -32
- data/lib/nodes/component_nodes.rb +0 -38
- data/lib/raindown/files.rb +0 -22
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be9fc3af39c4d6f20b00b691a14582626448ac903a01aa129a2f33911553a76f
|
|
4
|
+
data.tar.gz: d0bf1eea7f8996adb29d97eeeaeda1c38ad16739df7724704ab1402b1bd2c723
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6b4305e373de95235142d2e030849aac517d724232194bd68eb2c16cef7c81dd9594ccc8a80b2cd7d8042af045818773d1059c8d30caef634f0722ed35cd6d4
|
|
7
|
+
data.tar.gz: 593a219070643268ddd2c52a25aa34d0fe76d62f1074c2a3a263dddf72407d4b03c3a13113100e684ee19c6168a15525e059bf26c118ea6bca680c7112c3ee62
|
data/lib/elements.rb
CHANGED
|
@@ -6,28 +6,27 @@ module Raindown
|
|
|
6
6
|
class Elements
|
|
7
7
|
include Plugs
|
|
8
8
|
|
|
9
|
-
#
|
|
10
|
-
plug(:
|
|
9
|
+
# Write-in nodes, in <{ }>, like <{ Card }>, <{ :toc }> or <{ Card: }>...<{ :Card }>, rendered in the template pass.
|
|
10
|
+
plug(:write_in) do
|
|
11
11
|
plug(:lexeme) do
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
require_relative 'lexemes/node_lexeme'
|
|
13
|
+
NodeLexeme
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# A list: block loops over its files, so it comes before other nodes' blocks.
|
|
16
17
|
plug(:node) do
|
|
17
|
-
require_relative 'nodes/
|
|
18
|
-
|
|
18
|
+
require_relative 'nodes/list_node'
|
|
19
|
+
ListNode
|
|
19
20
|
end
|
|
20
|
-
end
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Antlers::SlotLexeme
|
|
22
|
+
plug(:node) do
|
|
23
|
+
require_relative 'nodes/write_in_node'
|
|
24
|
+
WriteInNode
|
|
26
25
|
end
|
|
27
26
|
|
|
28
27
|
plug(:node) do
|
|
29
|
-
require_relative 'nodes/
|
|
30
|
-
|
|
28
|
+
require_relative 'nodes/write_in_node'
|
|
29
|
+
SoloWriteInNode
|
|
31
30
|
end
|
|
32
31
|
end
|
|
33
32
|
|
|
@@ -42,17 +41,5 @@ module Raindown
|
|
|
42
41
|
EndpointNode
|
|
43
42
|
end
|
|
44
43
|
end
|
|
45
|
-
|
|
46
|
-
plug(:list) do
|
|
47
|
-
plug(:lexeme) do
|
|
48
|
-
require_relative 'lexemes/list_lexeme'
|
|
49
|
-
ListLexeme
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
plug(:node) do
|
|
53
|
-
require_relative 'nodes/list_node'
|
|
54
|
-
ListNode
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
44
|
end
|
|
58
45
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'antlers/api'
|
|
4
|
+
require_relative '../raindown/node_blocks'
|
|
5
|
+
|
|
6
|
+
module Raindown
|
|
7
|
+
# A write-in node, in Antlers' delimiters, read with its props as Raindown reads them, like
|
|
8
|
+
# <{ :list folder: 'notes' }>. It opens a block with <{ Name: }>, and <{ :Name }> ends one, or else it's self-closing,
|
|
9
|
+
# as is <{ Card title='Hi' }>. Template directives, like if: and for:, aren't nodes, so they're left to their lexemes.
|
|
10
|
+
module NodeLexeme
|
|
11
|
+
include Antlers::Lexeme
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
DIRECTIVE = /\A(:)?([A-Za-z]\w*(?:::[A-Z]\w*)*)(:)?(?:\s+(.*))?\z/m
|
|
16
|
+
|
|
17
|
+
# A name with a colon is a node, like <{ :toc }> or <{ :app_name }>, while {:app_name} is a translation. One without
|
|
18
|
+
# is a node when it's capitalized, so <{ title }> is still a variable. The lexer gives no name to a directive that
|
|
19
|
+
# starts with another lexeme's keyword, like <{ if: draft }> or <{ :list }>, which it leaves to that lexeme.
|
|
20
|
+
def match?(segment:, name: nil, **)
|
|
21
|
+
_, close, node_name, open = segment.to_s.match(DIRECTIVE).to_a
|
|
22
|
+
return false if name.nil? || node_name.nil? || (close && open)
|
|
23
|
+
return true if close || open
|
|
24
|
+
|
|
25
|
+
node_name.match?(/\A[A-Z]/)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# <{ :Name }> ends a block of the name, or else is self-closing, while <{ Name }> is always self-closing.
|
|
29
|
+
def lexeme(segment:, **)
|
|
30
|
+
_, close, name, open, args = segment.match(DIRECTIVE).to_a
|
|
31
|
+
return { node_def: name, args: args.to_s } if open
|
|
32
|
+
return { node_end: name, args: args.to_s } if close
|
|
33
|
+
|
|
34
|
+
{ node_solo: name, args: args.to_s }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/nodes/list_node.rb
CHANGED
|
@@ -1,46 +1,43 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'antlers/api'
|
|
4
|
+
require_relative '../raindown/context'
|
|
5
|
+
require_relative '../raindown/node_blocks'
|
|
6
|
+
require_relative '../raindown/nodes'
|
|
7
|
+
require_relative 'write_in_node'
|
|
4
8
|
|
|
5
9
|
module Raindown
|
|
10
|
+
# <{ list: page folder: 'notes' }> ... <{ :list }>, which renders its template for each file the list: node lists,
|
|
11
|
+
# with the file as page. Without a block, <{ :list folder: 'notes' }> renders the node's links, as a WriteInNode.
|
|
6
12
|
class ListNode < Antlers::BranchNode
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
DEF_KEY = :node_def
|
|
14
|
+
END_KEY = :node_end
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
def initialize(name:, args:)
|
|
17
|
+
super(name:)
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
super(name: :toc, props:, children:)
|
|
15
|
-
|
|
16
|
-
@value = value.delete("'").delete('"')
|
|
17
|
-
@folder = folder.delete("'").delete('"')
|
|
19
|
+
@variable, @tags = args.split(/\s+/, 2)
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
def render(current_binding: nil,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
current_binding.local_variable_set(@value, value)
|
|
28
|
-
|
|
29
|
-
@children.each do |child|
|
|
30
|
-
output += child.render(current_binding:, parent_binding:, slot_node:, metadata:) || ''
|
|
31
|
-
end
|
|
32
|
-
end
|
|
22
|
+
def render(current_binding: nil, context: Context.new, **)
|
|
23
|
+
tags = NodeBlocks.props(@tags.to_s, BoundVariables.new(current_binding))
|
|
24
|
+
Nodes.instance(Nodes.node_class('list'), tags).files(metadata: context.metadata).map do |file|
|
|
25
|
+
current_binding.local_variable_set(@variable, file)
|
|
26
|
+
super
|
|
27
|
+
end.join
|
|
28
|
+
end
|
|
33
29
|
|
|
34
|
-
|
|
30
|
+
def end_name
|
|
31
|
+
'list'
|
|
35
32
|
end
|
|
36
33
|
|
|
37
34
|
class << self
|
|
38
35
|
def match?(segment:)
|
|
39
|
-
segment[DEF_KEY]
|
|
36
|
+
segment[DEF_KEY] == 'list'
|
|
40
37
|
end
|
|
41
38
|
|
|
42
39
|
def build(segment:, **)
|
|
43
|
-
new(
|
|
40
|
+
new(name: :list, args: segment[:args])
|
|
44
41
|
end
|
|
45
42
|
end
|
|
46
43
|
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'antlers/api'
|
|
4
|
+
require 'nodes/html_node'
|
|
5
|
+
require_relative '../raindown/context'
|
|
6
|
+
require_relative '../raindown/fragments'
|
|
7
|
+
require_relative '../raindown/node_blocks'
|
|
8
|
+
require_relative '../raindown/nodes'
|
|
9
|
+
|
|
10
|
+
module Raindown
|
|
11
|
+
# A write-in node, in <{ }>, which Antlers renders where it is in the template, so its props see loop variables, like
|
|
12
|
+
# <{ Card name=p.name }> in a for: loop. It only renders, with its render, never its rewrite, and the Markdown
|
|
13
|
+
# between its tags, <{ Card: }> to <{ :Card }>, is its slot, rendered to HTML in its scope first.
|
|
14
|
+
# One with a class template, like Raindeer's .rbx components, renders its template, with that HTML as its slot.
|
|
15
|
+
class WriteInNode < Antlers::BranchNode
|
|
16
|
+
DEF_KEY = :node_def
|
|
17
|
+
END_KEY = :node_end
|
|
18
|
+
|
|
19
|
+
def initialize(name:, args: '', **)
|
|
20
|
+
super(name:)
|
|
21
|
+
|
|
22
|
+
@args = args
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render(current_binding: nil, context: Context.new, **)
|
|
26
|
+
slot = Raindown.commonmark(super) unless @children.empty?
|
|
27
|
+
props, args = NodeBlocks.args(@args, BoundVariables.new(current_binding))
|
|
28
|
+
node = Nodes.instance(node_class, props, args || [])
|
|
29
|
+
return Fragments.add(template_html(node, props, slot.to_s, current_binding)) if Nodes.template?(node.class)
|
|
30
|
+
|
|
31
|
+
Nodes.rendered(node, context, slot: slot.to_s)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def end_name
|
|
35
|
+
@name
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# A render-only component, like Card, isn't a node Nodes finds, as it has no rewrite, so it's any class of the name.
|
|
41
|
+
def node_class
|
|
42
|
+
klass = Nodes.node_class(@name)
|
|
43
|
+
return klass if klass
|
|
44
|
+
raise NameError, "No node is named #{@name}, for <{ :#{@name} }>" unless @name.match?(/\A[A-Z]/)
|
|
45
|
+
|
|
46
|
+
Object.const_get(@name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The node's class template, whose slot is the HTML in place of its children.
|
|
50
|
+
def template_html(node, props, slot, parent_binding)
|
|
51
|
+
children = @children
|
|
52
|
+
@children = [Antlers::HTMLNode.new(html: slot)]
|
|
53
|
+
node.render_template(parent_binding:, slot_node: self, props:)
|
|
54
|
+
ensure
|
|
55
|
+
@children = children
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
def match?(segment:)
|
|
60
|
+
segment[DEF_KEY]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build(segment:, **)
|
|
64
|
+
new(name: segment[DEF_KEY], args: segment[:args])
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# <{ Card }>, or <{ :Card }> when it doesn't end a block, which a lowercase name needs, like <{ :toc }>.
|
|
70
|
+
class SoloWriteInNode < WriteInNode
|
|
71
|
+
END_KEY = nil
|
|
72
|
+
|
|
73
|
+
def self.match?(segment:)
|
|
74
|
+
segment[:node_solo] || segment[:node_end]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.build(segment:, **)
|
|
78
|
+
new(name: segment[:node_solo] || segment[:node_end], args: segment[:args])
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The variables where Antlers renders a node, like a loop's, and then the front matter's, as its props read them.
|
|
83
|
+
class BoundVariables < BasicObject
|
|
84
|
+
def initialize(binding)
|
|
85
|
+
@binding = binding
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A name that can't be a local, like a-b, is only looked for in the front matter.
|
|
89
|
+
def send(name)
|
|
90
|
+
return @binding.local_variable_get(name) if name.match?(/\A[a-z_]\w*\z/) && @binding.local_variable_defined?(name)
|
|
91
|
+
|
|
92
|
+
@binding.receiver.send(name)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -8,22 +8,22 @@ require_relative 'front_matter'
|
|
|
8
8
|
module Raindown
|
|
9
9
|
# Raindown files (.rd/.raindown) can write a content directive, which writes its output back, bare on its own line,
|
|
10
10
|
# like endpoint: or toc:, which shows in bold in the HTML. Markdown files opt in with "raindown: true" in their front
|
|
11
|
-
# matter, or with the --bare flag. Nodes like toc: and
|
|
11
|
+
# matter, or with the --bare flag. Nodes like toc: and list: are handled by NodeBlocks, before the rest.
|
|
12
12
|
# Template directives, like if: and for:, are always in Antlers' delimiters, so they're never bare.
|
|
13
13
|
#
|
|
14
14
|
# A bare schema: shows in bold, with its fields in a code block below it.
|
|
15
15
|
#
|
|
16
|
-
# Bare directives have no end tag. A bare endpoint
|
|
17
|
-
#
|
|
16
|
+
# Bare directives have no end tag. A bare endpoint holds the lines straight below it, its curl snippet, the comment
|
|
17
|
+
# with its response's status, time and date, and its response, up to the next blank line, like a bare node block, so
|
|
18
|
+
# where it needs blank lines, write it in comments with an end tag instead:
|
|
18
19
|
# <!-- endpoint: -->
|
|
19
20
|
# ...
|
|
20
21
|
# <!-- :endpoint -->
|
|
21
22
|
module BareDirectives
|
|
22
23
|
EXTENSIONS = %w[.rd .raindown].freeze
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
LINE = /\A( {0,3})((#{BLOCK_KEYWORDS.keys.join('|')})(?:[ \t]+\S.*?)?)[ \t]*$/
|
|
24
|
+
# A bare endpoint, the one bare directive with a block. Four spaces of indent would make the line a Markdown code
|
|
25
|
+
# block.
|
|
26
|
+
LINE = /\A( {0,3})(endpoint:(?:[ \t]+\S.*?)?)[ \t]*$/
|
|
27
27
|
|
|
28
28
|
module_function
|
|
29
29
|
|
|
@@ -75,7 +75,7 @@ module Raindown
|
|
|
75
75
|
return index unless match
|
|
76
76
|
|
|
77
77
|
lines[index] = "#{Directives.marker(match[2], shown: true)}\n#{wrap(lines[index])}"
|
|
78
|
-
close_block(lines, index, indent: match[1]
|
|
78
|
+
close_block(lines, index, indent: match[1])
|
|
79
79
|
index
|
|
80
80
|
end
|
|
81
81
|
|
|
@@ -88,16 +88,10 @@ module Raindown
|
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
# The end tag after the block, then the end tag in bold, as a bare directive's shown.
|
|
91
|
-
def close_block(lines, index, indent
|
|
92
|
-
last =
|
|
91
|
+
def close_block(lines, index, indent:)
|
|
92
|
+
last = CodeBlocks.last_before_blank(lines, index)
|
|
93
93
|
lines[last] = "#{lines[last]}\n" unless lines[last].end_with?("\n")
|
|
94
|
-
|
|
95
|
-
lines.insert(last + 1, "#{indent}<{ :#{name} }>\n#{Directives.marker(":#{name}", shown: true)}\n")
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# The last line of the curl snippet and response below an endpoint.
|
|
99
|
-
def after_endpoint(lines, index)
|
|
100
|
-
CodeBlocks.endpoint(lines, index).last&.fetch(:close)
|
|
94
|
+
lines.insert(last + 1, "#{indent}<{ :endpoint }>\n#{Directives.marker(':endpoint', shown: true)}\n")
|
|
101
95
|
end
|
|
102
96
|
end
|
|
103
97
|
end
|
data/lib/raindown/chart/chart.rb
CHANGED
data/lib/raindown/code_blocks.rb
CHANGED
|
@@ -5,6 +5,9 @@ module Raindown
|
|
|
5
5
|
module CodeBlocks
|
|
6
6
|
FENCE = /\A {0,3}(`{3,}|~{3,})[ \t]*(\S*)/
|
|
7
7
|
SNIPPET_LANGUAGES = %w[sh bash shell zsh console curl].freeze
|
|
8
|
+
# The comment Raindown writes between an endpoint's snippet and its response, with the response's status, how long
|
|
9
|
+
# it took and the date it was fetched, like <!-- response 200 · 142 ms · 2026-10-14 -->.
|
|
10
|
+
RESPONSE_COMMENT = /\A {0,3}<!-- response [^\n]*? -->[ \t]*$/
|
|
8
11
|
# The line closing a fence, like ``` or ~~~~, compiled once per fence.
|
|
9
12
|
CLOSINGS = Hash.new { |closings, fence| closings[fence] = /\A {0,3}#{fence[0]}{#{fence.length},}[ \t]*$/ }
|
|
10
13
|
|
|
@@ -19,27 +22,32 @@ module Raindown
|
|
|
19
22
|
{ open: index, close:, language:, body: lines[index + 1...close].join } if close
|
|
20
23
|
end
|
|
21
24
|
|
|
22
|
-
# An endpoint's curl snippet and its response, if it has one yet
|
|
23
|
-
# A second snippet straight after is another example rather than the
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
# An endpoint's curl snippet and its response, if it has one yet, with the line of the response comment between them
|
|
26
|
+
# as the response's :comment, if there is one. A second snippet straight after is another example rather than the
|
|
27
|
+
# response. A bare endpoint's are in the lines straight below it, up to the next blank line.
|
|
28
|
+
def endpoint(lines, index, bare: false)
|
|
29
|
+
lines = lines[..last_before_blank(lines, index - 1)] if bare
|
|
30
|
+
snippet = at(lines, skip_blank_lines(lines, index))
|
|
31
|
+
return [] unless snippet
|
|
32
|
+
|
|
33
|
+
index = skip_blank_lines(lines, snippet[:close] + 1)
|
|
34
|
+
comment = index if lines[index]&.match?(RESPONSE_COMMENT)
|
|
35
|
+
response = at(lines, comment ? skip_blank_lines(lines, comment + 1) : index)
|
|
36
|
+
return [snippet] if response.nil? || snippet?(response)
|
|
37
|
+
|
|
38
|
+
[snippet, response.merge(comment:)]
|
|
27
39
|
end
|
|
28
40
|
|
|
29
41
|
def snippet?(block)
|
|
30
42
|
SNIPPET_LANGUAGES.include?(block[:language].downcase) || block[:body].lstrip.start_with?('curl')
|
|
31
43
|
end
|
|
32
44
|
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
index = block[:close] + 1
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
blocks
|
|
45
|
+
# The last of the lines straight below open, up to the next blank line, with fenced code kept whole, or open when
|
|
46
|
+
# there are none, like a bare block's.
|
|
47
|
+
def last_before_blank(lines, open)
|
|
48
|
+
last = open
|
|
49
|
+
last = at(lines, last + 1)&.fetch(:close) || (last + 1) while lines[last + 1] && !lines[last + 1].strip.empty?
|
|
50
|
+
last
|
|
43
51
|
end
|
|
44
52
|
|
|
45
53
|
def skip_blank_lines(lines, index)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'front_matter'
|
|
4
|
+
require_relative 'variables'
|
|
5
|
+
|
|
6
|
+
module Raindown
|
|
7
|
+
# The document being rendered, its metadata, like a site's pages, and whether its directives can be bare: the keywords
|
|
8
|
+
# a node's rewrite and render can take. Antlers passes it down to Raindown's own Antlers nodes.
|
|
9
|
+
Context = Data.define(:document, :metadata, :bare) do
|
|
10
|
+
def initialize(document: '', metadata: NoMetadata, bare: false)
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The document's front matter, as variables, like {title}.
|
|
15
|
+
def variables
|
|
16
|
+
Variables.new(FrontMatter.parse(document).first)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/raindown/directives.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Raindown
|
|
|
19
19
|
|
|
20
20
|
module_function
|
|
21
21
|
|
|
22
|
-
# A directive's line without its delimiters, like "
|
|
22
|
+
# A directive's line without its delimiters, like "list: folder: 'notes'" for <!-- list: folder: 'notes' -->.
|
|
23
23
|
def text(line)
|
|
24
24
|
line = line.strip
|
|
25
25
|
[COMMENT, DELIMITERS].each do |open, close|
|
data/lib/raindown/document.rb
CHANGED
|
@@ -16,6 +16,9 @@ module Raindown
|
|
|
16
16
|
class Document
|
|
17
17
|
Heading = Data.define(:level, :text, :line)
|
|
18
18
|
|
|
19
|
+
# Its Markdown, and whether it allows bare directives.
|
|
20
|
+
attr_reader :markdown, :bare
|
|
21
|
+
|
|
19
22
|
# A content directive: a node, like toc: or a project's Changelog:, or an endpoint.
|
|
20
23
|
# It spans line to end_line, its end tag, or the last line of a bare block.
|
|
21
24
|
# Body is the lines inside it, between it and its end tag, like an endpoint's request and response.
|
|
@@ -116,21 +119,18 @@ module Raindown
|
|
|
116
119
|
[bare.strip, true] if bare
|
|
117
120
|
end
|
|
118
121
|
|
|
119
|
-
# The response and the
|
|
122
|
+
# The response and the comment above it are the output, while the curl snippet above them is the author's.
|
|
120
123
|
def endpoint(open, args, writes)
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
bare = Directives.bare?(@lines[open])
|
|
125
|
+
curl, response = CodeBlocks.endpoint(@lines, open + 1, bare:)
|
|
126
|
+
output = response && ((response[:comment] || response[:open])..response[:close])
|
|
123
127
|
last = output&.end || curl&.fetch(:close) || open
|
|
124
|
-
# A bare endpoint
|
|
125
|
-
last = end_tag(after: last) || last
|
|
128
|
+
# A bare endpoint ends at the next blank line, and one in a comment at its end tag.
|
|
129
|
+
last = bare ? CodeBlocks.last_before_blank(@lines, open) : end_tag(after: last) || last
|
|
126
130
|
|
|
127
131
|
directive('endpoint', args, open..last, output:, writes:)
|
|
128
132
|
end
|
|
129
133
|
|
|
130
|
-
def fetched?(index)
|
|
131
|
-
@lines[index]&.match?(EndpointRunner::FETCHED)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
134
|
# The end tag after the endpoint's blocks, with only blank lines between.
|
|
135
135
|
def end_tag(after:)
|
|
136
136
|
index = CodeBlocks.skip_blank_lines(@lines, after + 1)
|
|
@@ -10,17 +10,17 @@ require_relative 'endpoint_variables'
|
|
|
10
10
|
|
|
11
11
|
module Raindown
|
|
12
12
|
# Runs the curl block under an endpoint directive in a comment, or a bare one, and writes the response in the code
|
|
13
|
-
# block below it,
|
|
13
|
+
# block below it, after a comment of its status, how long it took and the date it was fetched.
|
|
14
14
|
# Only runs when that block is missing or empty, and --execute is passed, so a file can't run requests by itself.
|
|
15
15
|
# Or with only: and a directive's line index, runs just that endpoint, replacing any response it has.
|
|
16
16
|
# <!-- endpoint: -->
|
|
17
17
|
# ```sh
|
|
18
18
|
# curl https://api.example.com
|
|
19
19
|
# ```
|
|
20
|
+
# <!-- response 200 · 142 ms · 2026-10-14 -->
|
|
20
21
|
# ```json
|
|
21
22
|
# { "id": 1 }
|
|
22
23
|
# ```
|
|
23
|
-
# <!-- raindown:fetched 2026-10-14 · 200 · 142 ms -->
|
|
24
24
|
# <!-- :endpoint -->
|
|
25
25
|
# Its snippet's {placeholders} are filled from EndpointVariables, and with as: login, later snippets can use its
|
|
26
26
|
# response, like {login.token}. With status: and schema:, each response in the file is checked, and failures are kept.
|
|
@@ -32,9 +32,7 @@ module Raindown
|
|
|
32
32
|
DIRECTIVE = /\A {0,3}<\{\s*endpoint:(.*?)\}>[ \t]*$/
|
|
33
33
|
COMMENT = /\A {0,3}<!--\s*endpoint:(.*?)-->[ \t]*$/
|
|
34
34
|
END_TAG = /\A {0,3}(?:<\{\s*:endpoint\s*\}>|<!--\s*:endpoint\s*-->)[ \t]*$/
|
|
35
|
-
|
|
36
|
-
FETCHED = /\A {0,3}<!-- raindown:fetched [^\n]*? -->[ \t]*$/
|
|
37
|
-
FETCHED_STATUS = /raindown:fetched \S+ · (\d{3}) /
|
|
35
|
+
RESPONSE_STATUS = /<!-- response (\d{3}) /
|
|
38
36
|
|
|
39
37
|
attr_reader :failures
|
|
40
38
|
|
|
@@ -83,19 +81,19 @@ module Raindown
|
|
|
83
81
|
|
|
84
82
|
# Returns the index of the last line it handled.
|
|
85
83
|
def fill(index, args, directive:)
|
|
86
|
-
curl, output = CodeBlocks.endpoint(@lines, index)
|
|
84
|
+
curl, output = CodeBlocks.endpoint(@lines, index, bare: !@lines[directive].match?(COMMENT))
|
|
87
85
|
return index - 1 unless curl&.dig(:body)&.lstrip&.start_with?('curl')
|
|
88
86
|
return write_output(curl:, output:, args:, directive:) if run?(output, directive)
|
|
89
87
|
return curl[:close] unless output
|
|
90
88
|
|
|
91
89
|
@variables.remember(args.name, output[:body])
|
|
92
|
-
check(args, directive:, body: output[:body], status:
|
|
90
|
+
check(args, directive:, body: output[:body], status: response_status(output))
|
|
93
91
|
curl[:close]
|
|
94
92
|
end
|
|
95
93
|
|
|
96
|
-
# The status in the
|
|
97
|
-
def
|
|
98
|
-
@lines[output[:
|
|
94
|
+
# The status in the comment above a response, which a comment written by hand may not have.
|
|
95
|
+
def response_status(output)
|
|
96
|
+
@lines[output[:comment]][RESPONSE_STATUS, 1]&.to_i if output[:comment]
|
|
99
97
|
end
|
|
100
98
|
|
|
101
99
|
# Run alone, only the endpoint that ran is checked. One without a response or a status hasn't run, so it has nothing
|
|
@@ -113,14 +111,15 @@ module Raindown
|
|
|
113
111
|
@execute && (output.nil? || output[:body].strip.empty?)
|
|
114
112
|
end
|
|
115
113
|
|
|
116
|
-
# Replaces
|
|
114
|
+
# Replaces everything from just after the curl block to the end of the output block, if there is one, like its
|
|
115
|
+
# comment.
|
|
117
116
|
# Secrets that a response echoes are written as their placeholders.
|
|
118
117
|
def write_output(curl:, output:, args:, directive:)
|
|
119
118
|
response = Curl.run(curl[:body], @variables)
|
|
120
119
|
body = @variables.redact(response.body)
|
|
121
120
|
@variables.remember(args.name, body)
|
|
122
121
|
check(args, directive:, body:, status: response.status)
|
|
123
|
-
replace_output(curl:, output:, block: [*output_block(body, response.language)
|
|
122
|
+
replace_output(curl:, output:, block: [response_comment(response), *output_block(body, response.language)])
|
|
124
123
|
rescue Curl::Error => e
|
|
125
124
|
message = @variables.redact(e.message)
|
|
126
125
|
# The one endpoint that was asked for fails for the caller to show, like in the terminal UI.
|
|
@@ -132,21 +131,13 @@ module Raindown
|
|
|
132
131
|
|
|
133
132
|
# Returns the index of the block's last line.
|
|
134
133
|
def replace_output(curl:, output:, block:)
|
|
135
|
-
|
|
136
|
-
start
|
|
137
|
-
|
|
138
|
-
@lines[start, output ? replaced_length(output) : 0] = block
|
|
134
|
+
start = curl[:close] + 1
|
|
135
|
+
@lines[start, output ? output[:close] - start + 1 : 0] = block
|
|
139
136
|
start + block.length - 1
|
|
140
137
|
end
|
|
141
138
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
length = output[:close] - output[:open] + 1
|
|
145
|
-
@lines[output[:close] + 1]&.match?(FETCHED) ? length + 1 : length
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def fetched(response)
|
|
149
|
-
"<!-- raindown:fetched #{Date.today.iso8601} · #{response.status} · #{response.milliseconds} ms -->\n"
|
|
139
|
+
def response_comment(response)
|
|
140
|
+
"<!-- response #{response.status} · #{response.milliseconds} ms · #{Date.today.iso8601} -->\n"
|
|
150
141
|
end
|
|
151
142
|
|
|
152
143
|
# The fence is longer than any run of backticks in the body, so the body can't close it.
|
data/lib/raindown/fragments.rb
CHANGED
|
@@ -18,8 +18,13 @@ module Raindown
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def add(html)
|
|
21
|
+
"<!-- raindown:fragment #{store(html)} -->"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Keeps the HTML for this render, returning its index in the list.
|
|
25
|
+
def store(html)
|
|
21
26
|
list << html
|
|
22
|
-
|
|
27
|
+
list.length - 1
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
# Fills each placeholder, again and again, since a fragment can hold others, like a component in a slot.
|
data/lib/raindown/js/host.mjs
CHANGED
|
@@ -16,13 +16,13 @@ const classes = {}
|
|
|
16
16
|
const nodes = {}
|
|
17
17
|
|
|
18
18
|
const requests = {
|
|
19
|
-
// Every exported class with
|
|
19
|
+
// Every exported class with rewrite is a node, named by its export, or by its class for a default export.
|
|
20
20
|
async load({ folder }) {
|
|
21
21
|
for (const file of readdirSync(folder).filter((file) => /\.m?js$/.test(file))) {
|
|
22
22
|
const exports = await import(pathToFileURL(join(folder, file)).href)
|
|
23
23
|
|
|
24
24
|
for (const [name, value] of Object.entries(exports)) {
|
|
25
|
-
if (typeof value?.prototype?.
|
|
25
|
+
if (typeof value?.prototype?.rewrite === 'function') classes[name === 'default' ? value.name : name] = value
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
data/lib/raindown/js/js_nodes.rb
CHANGED
|
@@ -6,7 +6,7 @@ require 'open3'
|
|
|
6
6
|
require_relative '../node'
|
|
7
7
|
|
|
8
8
|
module Raindown
|
|
9
|
-
# JavaScript Components, the .js and .mjs files in a project's .raindown folder, which export classes with
|
|
9
|
+
# JavaScript Components, the .js and .mjs files in a project's .raindown folder, which export classes with rewrite and
|
|
10
10
|
# optionally render, like Ruby nodes. They run in a NodeJS process of Raindown's own, started when a folder has them,
|
|
11
11
|
# so only projects with JavaScript Components need NodeJS.
|
|
12
12
|
module JSNodes
|
|
@@ -57,7 +57,7 @@ module Raindown
|
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
# A JavaScript Component, made in the NodeJS process with its props, where its
|
|
60
|
+
# A JavaScript Component, made in the NodeJS process with its props, where its rewrite and render are sent.
|
|
61
61
|
class JSNode < Node
|
|
62
62
|
# A class for each JavaScript Component, which renders its own HTML when the component has render.
|
|
63
63
|
def self.for(name:, render:)
|
|
@@ -72,8 +72,8 @@ module Raindown
|
|
|
72
72
|
JSNodes.host.request(op: 'new', id: object_id, name: self.class.name, args:, props:)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
def
|
|
76
|
-
js_output(:
|
|
75
|
+
def rewrite(**context)
|
|
76
|
+
js_output(:rewrite, context)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
private
|