raindown 0.13.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 +1 -1
- data/lib/raindown/chart/chart.rb +1 -1
- data/lib/raindown/context.rb +19 -0
- data/lib/raindown/directives.rb +1 -1
- 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 +27 -21
- data/lib/raindown/node_injector.rb +1 -1
- data/lib/raindown/nodes.rb +60 -22
- data/lib/raindown/pages.rb +8 -4
- data/lib/raindown/pre_renderer.rb +13 -10
- data/lib/raindown/stat.rb +1 -1
- data/lib/raindown/table/markdown_table.rb +2 -0
- data/lib/raindown/table/table.rb +19 -5
- data/lib/raindown/toc.rb +2 -2
- data/lib/raindown/wiki_links.rb +1 -1
- data/lib/raindown.rb +9 -14
- data/lib/version.rb +1 -1
- metadata +6 -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,7 +8,7 @@ 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.
|
data/lib/raindown/chart/chart.rb
CHANGED
|
@@ -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/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
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'node'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
# The node behind list:, the files the metadata has for its props, like list: folder: 'notes'. Bare or in comments it
|
|
7
|
+
# writes a link to each, and in <{ list: page folder: 'notes' }> ... <{ :list }> it renders its template for each,
|
|
8
|
+
# with the file as page, the name before its props, which is its argument. A project can change how files are listed
|
|
9
|
+
# with its own List node in .raindown, like "class List < Raindown::List".
|
|
10
|
+
class List < Node
|
|
11
|
+
# Order is the metadata to sort by, like order: :created_at, rather than a tag to list by.
|
|
12
|
+
def initialize(_name = nil, order: nil, **tags)
|
|
13
|
+
@order = order
|
|
14
|
+
@tags = tags
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Each file has its front matter, its path and its content. Sorted by an order, files without it come last, and ones
|
|
18
|
+
# with the same value stay in the order they're listed.
|
|
19
|
+
def files(metadata: NoMetadata)
|
|
20
|
+
files = metadata.list(**@tags)
|
|
21
|
+
return files unless @order
|
|
22
|
+
|
|
23
|
+
sorted, unsorted = files.partition { it[@order] }
|
|
24
|
+
sorted.sort_by.with_index { |file, index| [file[@order], index] } + unsorted
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def rewrite(metadata: NoMetadata)
|
|
28
|
+
files(metadata:).map { |file| "- [#{file.title || file.path}](#{file.path})" }.join("\n")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -57,10 +57,11 @@ module Raindown
|
|
|
57
57
|
|
|
58
58
|
# Braces that the template pass hid, in its code or its props' values, are written back as braces.
|
|
59
59
|
# The template pass loads here, as it loads nodes itself.
|
|
60
|
-
def
|
|
60
|
+
def rewrite(document: '', metadata: NoMetadata, bare: false)
|
|
61
61
|
require_relative 'pre_renderer'
|
|
62
62
|
defaults, template = FrontMatter.parse(File.read(self.class.path))
|
|
63
|
-
|
|
63
|
+
context = Context.new(document:, metadata:, bare:)
|
|
64
|
+
markdown = PreRenderer.render(template, variables: Variables.new(defaults.merge(@props)), context:)
|
|
64
65
|
Variables.unwrap(markdown.sub(/\A(?:[ \t]*\n)+/, '').rstrip)
|
|
65
66
|
end
|
|
66
67
|
end
|
data/lib/raindown/node.rb
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Raindown
|
|
4
|
-
# The base class of Raindown's own nodes, like TOC and
|
|
4
|
+
# The base class of Raindown's own nodes, like TOC and List.
|
|
5
5
|
# A project's nodes are LowNodes, or subclasses of these.
|
|
6
|
-
class Node
|
|
6
|
+
class Node
|
|
7
|
+
# Write-in, in <{ }>, a node renders its Markdown as HTML. One whose rewrite takes written:, what it wrote last
|
|
8
|
+
# time, can write edits back to its source, like Table to its file, so it needs its own render, which never does.
|
|
9
|
+
def render(**context)
|
|
10
|
+
raise Nodes::NoRender, "#{self.class.name} writes edits back, so it needs a render" if Nodes.takes?(self, :rewrite, :written)
|
|
11
|
+
|
|
12
|
+
Raindown.commonmark(Nodes.call(self, :rewrite, **context).to_s)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
7
15
|
end
|
data/lib/raindown/node_blocks.rb
CHANGED
|
@@ -16,9 +16,10 @@ module Raindown
|
|
|
16
16
|
# - **1.2.0** Bare directives
|
|
17
17
|
#
|
|
18
18
|
# <!-- :Changelog -->
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# a
|
|
19
|
+
# A bare :Changelog is write-in: it only renders, with its render, never its rewrite. In Antlers' delimiters, like
|
|
20
|
+
# <{ Changelog }>, it's write-in too, and the template pass renders it, as a WriteInNode, so it's only found here
|
|
21
|
+
# for a document's outline. toc: and list: are Raindown's own nodes. A name only starts a block when it's a loaded
|
|
22
|
+
# node, so other text like "Note:" is left alone.
|
|
22
23
|
module NodeBlocks
|
|
23
24
|
class MissingEndTag < StandardError; end
|
|
24
25
|
class BrokenText < StandardError; end
|
|
@@ -27,7 +28,8 @@ module Raindown
|
|
|
27
28
|
OPEN = /\A( {0,3})<\{\s*(#{NAME}):(?:\s+(.*?))?\s*\}>[ \t]*$/
|
|
28
29
|
COMMENT_OPEN = /\A( {0,3})<!--\s*(#{NAME}):(?:\s+(.*?))?\s*-->[ \t]*$/
|
|
29
30
|
BARE_OPEN = /\A( {0,3})(#{NAME}):(?:[ \t]+(.*?))?[ \t]*$/
|
|
30
|
-
|
|
31
|
+
# <{ Card }>, or <{ :Card }>, which a lowercase name like toc needs, as <{ toc }> is a variable.
|
|
32
|
+
SELF_CLOSING = /\A( {0,3})<\{\s*(?::|(?=[A-Z]))(#{NAME})(?:\s+(.*?))?\s*\}>[ \t]*$/
|
|
31
33
|
BARE_SELF_CLOSING = /\A( {0,3}):(#{NAME})(?:[ \t]+(.*?))?[ \t]*$/
|
|
32
34
|
# Unquoted text after name:, like YAML's, runs up to the next prop, so title: Hello world limit: 3 has two.
|
|
33
35
|
UNQUOTED = /.+?(?=[ \t]+\w+(?:=|:[ \t])|[ \t]*\z)/
|
|
@@ -40,12 +42,15 @@ module Raindown
|
|
|
40
42
|
module_function
|
|
41
43
|
|
|
42
44
|
# Each block outside code and front matter as { open:, close:, last:, writes:, node: }, where close is nil for a
|
|
43
|
-
# bare block, and writes is whether its output's written into the file. A self-closing node is a block of one line
|
|
44
|
-
# with self_closing: true.
|
|
45
|
+
# bare block, and writes is whether its output's written into the file. A self-closing node is a block of one line.
|
|
45
46
|
def find(lines, front_matter: {}, bare: false)
|
|
47
|
+
with_nodes(spans(lines, bare:), front_matter)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def with_nodes(spans, front_matter)
|
|
46
51
|
variables = Variables.new(front_matter)
|
|
47
52
|
|
|
48
|
-
spans
|
|
53
|
+
spans.map do |span|
|
|
49
54
|
node = Nodes.instance(span[:class], *args(span[:args], variables, antlers: span[:antlers]))
|
|
50
55
|
span.except(:name, :class, :args, :antlers).merge(node:)
|
|
51
56
|
end
|
|
@@ -71,28 +76,29 @@ module Raindown
|
|
|
71
76
|
spans
|
|
72
77
|
end
|
|
73
78
|
|
|
74
|
-
# Swaps each block for its node's
|
|
75
|
-
# The directive and end tag become Directives markers, which also keep the node's
|
|
76
|
-
# the Markdown around it, like two lists becoming one.
|
|
77
|
-
def to_rendered(
|
|
78
|
-
lines =
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
# Swaps each bare block or block in comments for its node's Markdown, or its HTML when it has its own, or only renders,
|
|
80
|
+
# as a fragment or a placeholder. The directive and end tag become Directives markers, which also keep the node's
|
|
81
|
+
# output from joining the Markdown around it, like two lists becoming one. Ones in <{ }> are left to Antlers.
|
|
82
|
+
def to_rendered(context)
|
|
83
|
+
lines = context.document.dup.force_encoding('UTF-8').lines
|
|
84
|
+
spans = spans(lines, bare: context.bare).reject { it[:antlers] }
|
|
85
|
+
with_nodes(spans, FrontMatter.parse(context.document).first).reverse_each do |block|
|
|
86
|
+
lines[block[:open]..block[:last]] = rendered_lines(lines, block, context)
|
|
81
87
|
end
|
|
82
88
|
lines.join
|
|
83
89
|
end
|
|
84
90
|
|
|
85
|
-
def rendered_lines(lines, block,
|
|
86
|
-
output = node_markdown(block
|
|
91
|
+
def rendered_lines(lines, block, context)
|
|
92
|
+
output = node_markdown(block, context).chomp
|
|
87
93
|
close = marker(lines[block[:close]]) if block[:close]
|
|
88
94
|
|
|
89
95
|
[marker(lines[block[:open]]), *("#{output}\n".lines unless output.empty?), *close]
|
|
90
96
|
end
|
|
91
97
|
|
|
92
|
-
def node_markdown(
|
|
93
|
-
return Nodes.
|
|
98
|
+
def node_markdown(block, context)
|
|
99
|
+
return Nodes.rendered(block[:node], context) if !block[:writes] || Nodes.html?(block[:node])
|
|
94
100
|
|
|
95
|
-
Nodes.call(node, :
|
|
101
|
+
Nodes.call(block[:node], :rewrite, **context.to_h).to_s
|
|
96
102
|
end
|
|
97
103
|
|
|
98
104
|
def marker(line)
|
|
@@ -109,7 +115,7 @@ module Raindown
|
|
|
109
115
|
end_tag(lines, index, found[2], comment: pattern == :comment_open)
|
|
110
116
|
.then { |close| { open: index, close:, last: close, writes: pattern == :comment_open, **span } }
|
|
111
117
|
when :bare_open then { **bare_block(lines, index), writes: true, **span }
|
|
112
|
-
else { open: index, close: nil, last: index, writes: false,
|
|
118
|
+
else { open: index, close: nil, last: index, writes: false, **span }
|
|
113
119
|
end
|
|
114
120
|
end
|
|
115
121
|
|
|
@@ -156,7 +162,7 @@ module Raindown
|
|
|
156
162
|
end
|
|
157
163
|
|
|
158
164
|
# A value, like 'text', 3, true, nil or :draft, or else a variable. In Antlers' delimiters a word is a variable, like
|
|
159
|
-
# author.name, while bare or in a comment, which read like Markdown, it's text, like notes in
|
|
165
|
+
# author.name, while bare or in a comment, which read like Markdown, it's text, like notes in list: folder: notes,
|
|
160
166
|
# and only one in braces is a variable, like {author.name}.
|
|
161
167
|
def value(value, variables, antlers: true)
|
|
162
168
|
Lowkey::ValueParser.parse(value) do
|
|
@@ -36,7 +36,7 @@ module Raindown
|
|
|
36
36
|
|
|
37
37
|
def block_lines(lines, block)
|
|
38
38
|
context = { document: @content, metadata: @metadata, bare: @bare, written: written_lines(lines, block) }
|
|
39
|
-
markdown = Nodes.call(block[:node], :
|
|
39
|
+
markdown = Nodes.call(block[:node], :rewrite, **context).to_s.chomp
|
|
40
40
|
written = markdown.empty? ? [] : "#{markdown}\n".lines
|
|
41
41
|
return [line(lines[block[:open]]), *bare(written, block)] unless block[:close]
|
|
42
42
|
|
data/lib/raindown/nodes.rb
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'chart/chart'
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'fragments'
|
|
5
5
|
require_relative 'js/js_nodes'
|
|
6
|
+
require_relative 'list'
|
|
6
7
|
require_relative 'markdown_nodes'
|
|
7
8
|
require_relative 'node'
|
|
8
9
|
require_relative 'stat'
|
|
@@ -13,15 +14,20 @@ module Raindown
|
|
|
13
14
|
# Nodes are LowNodes that a node block writes into the Markdown and renders into the HTML.
|
|
14
15
|
# A project keeps them in a .raindown folder, which is loaded with Dirload, beside its JavaScript and Markdown
|
|
15
16
|
# Components, if any.
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
17
|
+
# Bare or in comments, a node writes back: its Markdown, from #rewrite, is written into the file, and its HTML is from
|
|
18
|
+
# #render if it has one. In <{ }> it's write-in: it only renders, with #render, which takes the Markdown between its
|
|
19
|
+
# tags as slot:, in HTML. Either can take the keywords document: (the Markdown being rendered), metadata:, bare: and
|
|
20
|
+
# written: (the lines it wrote last time), and gets the ones it asks for.
|
|
19
21
|
module Nodes
|
|
22
|
+
# A write-in node that has no render.
|
|
23
|
+
class NoRender < StandardError; end
|
|
24
|
+
|
|
20
25
|
FOLDER = '.raindown'
|
|
21
|
-
# Lowercase directives for Raindown's own nodes. A project's own TOC,
|
|
22
|
-
BUILT_INS = { 'toc' => 'TOC', '
|
|
23
|
-
# A placeholder for a node's HTML, which stays as it is through Commonmarker and Antlers
|
|
24
|
-
|
|
26
|
+
# Lowercase directives for Raindown's own nodes. A project's own TOC, List, Chart or Table node replaces Raindown's.
|
|
27
|
+
BUILT_INS = { 'toc' => 'TOC', 'list' => 'List', 'chart' => 'Chart', 'table' => 'Table' }.freeze
|
|
28
|
+
# A placeholder for a node's HTML, which stays as it is through Commonmarker and Antlers, with its slot's HTML, as a
|
|
29
|
+
# fragment, when it has one.
|
|
30
|
+
PLACEHOLDER = /<!-- raindown:node (\d+)(?: slot (\d+))? -->/
|
|
25
31
|
|
|
26
32
|
module_function
|
|
27
33
|
|
|
@@ -63,21 +69,21 @@ module Raindown
|
|
|
63
69
|
|
|
64
70
|
# A Ruby node of the name, or else a JavaScript one, or else a Markdown one.
|
|
65
71
|
def loaded_class(name)
|
|
66
|
-
klass = Object.const_get(name) if Object.const_defined?(name)
|
|
72
|
+
klass = Object.const_get(name) if name.match?(/\A[A-Z]/) && Object.const_defined?(name)
|
|
67
73
|
return klass if node?(klass)
|
|
68
74
|
|
|
69
75
|
JSNodes.node_class(name) || MarkdownNodes.node_class(name)
|
|
70
76
|
end
|
|
71
77
|
|
|
72
78
|
# A LowNode, or one of Raindown's own nodes, that writes Markdown. In an app, LowNodes are also routes and pages,
|
|
73
|
-
# which don't have
|
|
79
|
+
# which don't have rewrite, so text like "Note:" only starts a block when the Note node writes something.
|
|
74
80
|
def node?(klass)
|
|
75
|
-
return false unless klass.is_a?(Class) && klass.method_defined?(:
|
|
81
|
+
return false unless klass.is_a?(Class) && klass.method_defined?(:rewrite)
|
|
76
82
|
|
|
77
83
|
klass < Node || (defined?(::LowNode) && klass < ::LowNode) || false
|
|
78
84
|
end
|
|
79
85
|
|
|
80
|
-
# Calls a node's
|
|
86
|
+
# Calls a node's rewrite or render with the context keywords it takes.
|
|
81
87
|
def call(node, name, **context)
|
|
82
88
|
parameters = node.method(name).parameters
|
|
83
89
|
return node.public_send(name, **context) if parameters.any? { |type, _| type == :keyrest }
|
|
@@ -99,24 +105,56 @@ module Raindown
|
|
|
99
105
|
list[instances[key]]
|
|
100
106
|
end
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
# Renders the node where it is, as a fragment, so its headings are part of the page, or through a placeholder, once
|
|
109
|
+
# the page is HTML, when its render takes html:, the page's HTML, like TOC's.
|
|
110
|
+
def rendered(node, context, slot: '')
|
|
111
|
+
raise NoRender, "#{node.class.name} has no render, so it can't be write-in: write it bare or in comments" unless renders?(node)
|
|
112
|
+
return placeholder(node, slot:) if !template?(node.class) && takes?(node, :render, :html)
|
|
113
|
+
|
|
114
|
+
Fragments.add(html(node, context, slot:))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def takes?(node, method_name, keyword)
|
|
118
|
+
node.method(method_name).parameters.any? { |_, name| name == keyword }
|
|
104
119
|
end
|
|
105
120
|
|
|
106
|
-
#
|
|
107
|
-
def
|
|
108
|
-
|
|
121
|
+
# Whether the node's class has a template, like a Raindeer .rbx component, which renders in place of render.
|
|
122
|
+
def template?(klass)
|
|
123
|
+
klass.respond_to?(:template) && !klass.template.nil?
|
|
109
124
|
end
|
|
110
125
|
|
|
111
|
-
def
|
|
112
|
-
return node.
|
|
126
|
+
def placeholder(node, slot: '')
|
|
127
|
+
return "<!-- raindown:node #{list.index(node)} -->" if slot.empty?
|
|
113
128
|
|
|
114
|
-
|
|
129
|
+
"<!-- raindown:node #{list.index(node)} slot #{Fragments.store(slot)} -->"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Swaps each placeholder for its node's HTML, given the page's HTML and its slot, the HTML between its tags in <{ }>,
|
|
133
|
+
# which can hold components' fragments.
|
|
134
|
+
def render_placeholders(page_html, context)
|
|
135
|
+
page_html.gsub(PLACEHOLDER) do
|
|
136
|
+
match = ::Regexp.last_match
|
|
137
|
+
slot = match[2] ? Fragments.list[match[2].to_i] : ''
|
|
138
|
+
Fragments.fill(html(list[match[1].to_i], context, slot:, html: page_html))
|
|
139
|
+
end
|
|
115
140
|
end
|
|
116
141
|
|
|
117
|
-
|
|
142
|
+
def html(node, context, **keywords)
|
|
143
|
+
return node.render_template(props: {}).to_s if template?(node.class)
|
|
144
|
+
|
|
145
|
+
call(node, :render, **context.to_h, **keywords).to_s
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Whether the node renders its own HTML rather than the HTML of its Markdown, which Raindown's nodes render by default.
|
|
118
149
|
def html?(node)
|
|
119
|
-
|
|
150
|
+
return true if template?(node.class)
|
|
151
|
+
|
|
152
|
+
node.respond_to?(:render) && node.method(:render).owner != Node
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Whether the node renders at all, so it can be write-in, in <{ }>.
|
|
156
|
+
def renders?(node)
|
|
157
|
+
node.respond_to?(:render) || html?(node)
|
|
120
158
|
end
|
|
121
159
|
|
|
122
160
|
def instances
|
data/lib/raindown/pages.rb
CHANGED
|
@@ -5,7 +5,7 @@ require_relative 'page'
|
|
|
5
5
|
require_relative 'utils'
|
|
6
6
|
|
|
7
7
|
module Raindown
|
|
8
|
-
# The metadata of a project's Markdown files, which list:
|
|
8
|
+
# The metadata of a project's Markdown files, which list: lists by tag.
|
|
9
9
|
# Ported from Raindeer's Rain::Pages, and takes the same Dirload metadata.
|
|
10
10
|
#
|
|
11
11
|
# Each file is tagged with its front matter, like "tags: [ruby, docs]" or "date: 2099", and by its path,
|
|
@@ -65,7 +65,7 @@ module Raindown
|
|
|
65
65
|
def initialize(root:, metadata:, render_options: {}, eager: false)
|
|
66
66
|
@root = root
|
|
67
67
|
@render_options = render_options
|
|
68
|
-
@file_paths = metadata
|
|
68
|
+
@file_paths = filtered_paths(metadata:)
|
|
69
69
|
load_metadata if eager
|
|
70
70
|
end
|
|
71
71
|
|
|
@@ -82,6 +82,11 @@ module Raindown
|
|
|
82
82
|
|
|
83
83
|
private
|
|
84
84
|
|
|
85
|
+
# The Markdown files in Dirload's metadata, only those under the root path.
|
|
86
|
+
def filtered_paths(metadata:)
|
|
87
|
+
metadata.file_types.values_at(*EXTENSIONS).flatten.compact.select { it.start_with?("#{@root}/") }
|
|
88
|
+
end
|
|
89
|
+
|
|
85
90
|
def metadata
|
|
86
91
|
load_metadata unless @metadata
|
|
87
92
|
@metadata
|
|
@@ -103,8 +108,7 @@ module Raindown
|
|
|
103
108
|
end
|
|
104
109
|
|
|
105
110
|
def process(file_path:)
|
|
106
|
-
metadata = implicit_metadata(file_path:).merge(front_matter(file_path))
|
|
107
|
-
@metadata[file_path] = metadata
|
|
111
|
+
metadata = @metadata[file_path] = implicit_metadata(file_path:).merge(front_matter(file_path))
|
|
108
112
|
metadata.each { |type, tag| Array(tag).each { |item| tag(type:, tag: item, file_path:) } }
|
|
109
113
|
end
|
|
110
114
|
|
|
@@ -5,31 +5,33 @@ require_relative '../elements'
|
|
|
5
5
|
require_relative 'bare_directives'
|
|
6
6
|
require_relative 'code'
|
|
7
7
|
require_relative 'code_blocks'
|
|
8
|
+
require_relative 'context'
|
|
8
9
|
require_relative 'front_matter'
|
|
9
10
|
|
|
10
11
|
module Raindown
|
|
11
12
|
# Renders the directives whose output is Markdown, before Commonmarker reads it: variables, loops, conditions and lists.
|
|
12
13
|
# So a loop's items become one Markdown list or table, and a heading's id and its entry in a table of contents
|
|
13
14
|
# come from its text, like "# Notes for {name}" becoming "Notes for Mark" with the id "notes-for-mark".
|
|
14
|
-
# Nodes like toc: and
|
|
15
|
-
# that Raindown puts back once the document is HTML.
|
|
15
|
+
# Nodes bare or in comments, like toc: and list:, render before this. Nodes in <{ }> and endpoints render here, as
|
|
16
|
+
# fragments of HTML that Raindown puts back once the document is HTML.
|
|
16
17
|
module PreRenderer
|
|
17
18
|
BLOCK_DIRECTIVES = %w[for if list].freeze
|
|
18
19
|
# A for, if or list directive alone on its line, which leaves no line behind, so a loop's list items stay together.
|
|
19
|
-
|
|
20
|
+
# A self-closing <{ :list folder: 'notes' }> isn't one, as only an open tag has props after its name.
|
|
21
|
+
BLOCK_LINE = /^[ \t]*(<\{\s*(?:(?:#{BLOCK_DIRECTIVES.join('|')}):[^}]*|:(?:#{BLOCK_DIRECTIVES.join('|')})\s*)\}>)[ \t]*\n/
|
|
20
22
|
# An endpoint's directive or end tag in a comment, which renders as it would in Antlers' delimiters.
|
|
21
23
|
ENDPOINT_COMMENT = /\A( {0,3})<!--\s*(endpoint:.*?|:endpoint)\s*-->/
|
|
22
24
|
|
|
23
25
|
module_function
|
|
24
26
|
|
|
25
|
-
def render(markdown, variables:,
|
|
26
|
-
markdown = BareDirectives.expand(markdown) if bare
|
|
27
|
+
def render(markdown, variables:, context: Context.new)
|
|
28
|
+
markdown = BareDirectives.expand(markdown) if context.bare
|
|
27
29
|
markdown = delimited_endpoints(markdown) if markdown.include?('<!--')
|
|
28
30
|
return markdown unless markdown.include?('{')
|
|
29
31
|
|
|
30
32
|
markdown = Code.hide_braces(markdown.dup.force_encoding('UTF-8'))
|
|
31
33
|
body = FrontMatter.parse(markdown).last
|
|
32
|
-
"#{markdown.delete_suffix(body)}#{render_body(body, variables,
|
|
34
|
+
"#{markdown.delete_suffix(body)}#{render_body(body, variables, context)}"
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
# The endpoints in comments in Antlers' delimiters, which the template pass renders, leaving code as it is.
|
|
@@ -46,16 +48,17 @@ module Raindown
|
|
|
46
48
|
lines.join
|
|
47
49
|
end
|
|
48
50
|
|
|
49
|
-
def render_body(body, variables,
|
|
51
|
+
def render_body(body, variables, context)
|
|
50
52
|
template = body.gsub(BLOCK_LINE, '\1')
|
|
51
53
|
return body unless template.include?('{')
|
|
52
54
|
|
|
53
55
|
# Antlers strips the whitespace around a template, which can be Markdown, like an indented code block.
|
|
54
56
|
leading = template[/\A\s*/]
|
|
55
57
|
trailing = template[/\s*\z/]
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
# Write-in nodes and endpoints render here too, where loop variables are in scope, and keep their HTML as
|
|
59
|
+
# fragments.
|
|
60
|
+
elements = Antlers::Elements[:html, :var, :if, :for] + Elements[:write_in, :endpoint]
|
|
61
|
+
rendered = Antlers.render(ast: Antlers.ast(template:, elements:), current_binding: variables.binding, context:)
|
|
59
62
|
|
|
60
63
|
"#{leading}#{rendered}#{trailing}"
|
|
61
64
|
end
|
data/lib/raindown/stat.rb
CHANGED
|
@@ -23,7 +23,7 @@ module Raindown
|
|
|
23
23
|
'9' => ['█▀█', '▀▀█', '▀▀▀'], '-' => [' ', '▀▀▀', ' '], '+' => [' ▄ ', '▀█▀', ' '],
|
|
24
24
|
'.' => [' ', ' ', '▀'], ',' => [' ', ' ', '▀'] }.freeze
|
|
25
25
|
|
|
26
|
-
def
|
|
26
|
+
def rewrite(metadata: NoMetadata) = "```\n#{card_lines(metadata:).join("\n")}\n```"
|
|
27
27
|
|
|
28
28
|
# The number in block digits, then the label, as a card on the Dashboard shows them.
|
|
29
29
|
def card_lines(metadata: NoMetadata)
|
|
@@ -11,6 +11,8 @@ module Raindown
|
|
|
11
11
|
module_function
|
|
12
12
|
|
|
13
13
|
def format(rows)
|
|
14
|
+
return '' if rows.empty?
|
|
15
|
+
|
|
14
16
|
header, *body = rows.map { |row| "| #{row.map { escape(it) }.join(' | ')} |" }
|
|
15
17
|
[header, "|#{Array.new(rows.first.length, '---').join('|')}|", *body].join("\n")
|
|
16
18
|
end
|
data/lib/raindown/table/table.rb
CHANGED
|
@@ -27,16 +27,30 @@ module Raindown
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# Written is what it wrote last time, so it can tell which of the table and the file changed since.
|
|
30
|
-
def
|
|
31
|
-
path =
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
def rewrite(written: [], metadata: NoMetadata)
|
|
31
|
+
path = data_path(metadata)
|
|
32
|
+
data_error(path) || synced(path, written.map(&:chomp))
|
|
33
|
+
end
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
# Write-in, in <{ }>, the table only shows its file, and never writes to it.
|
|
36
|
+
def render(metadata: NoMetadata)
|
|
37
|
+
path = data_path(metadata)
|
|
38
|
+
Raindown.commonmark(data_error(path) || MarkdownTable.format(read_rows(path)))
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
private
|
|
39
42
|
|
|
43
|
+
def data_path(metadata)
|
|
44
|
+
File.expand_path(@data.to_s, metadata.respond_to?(:root) ? metadata.root : Dir.pwd)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Why the file can't be a table, or nil when it can.
|
|
48
|
+
def data_error(path)
|
|
49
|
+
return "Can't show #{@data.inspect} as a table: only CSV and TSV files can be" unless SEPARATORS.key?(File.extname(path).downcase)
|
|
50
|
+
|
|
51
|
+
"Can't show #{@data.inspect} as a table: no such file" unless File.file?(path)
|
|
52
|
+
end
|
|
53
|
+
|
|
40
54
|
def synced(path, written)
|
|
41
55
|
file = read_rows(path)
|
|
42
56
|
synced_hash = written.first&.[](COMMENT, 1)
|
data/lib/raindown/toc.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Raindown
|
|
|
14
14
|
IGNORED = %r{<!--.*?-->|<script\b.*?</script\s*>}mi
|
|
15
15
|
TAG = /<[^>]*>/
|
|
16
16
|
|
|
17
|
-
def
|
|
17
|
+
def rewrite(document:, metadata: nil, bare: false)
|
|
18
18
|
headings = headings(markdown: document, metadata:, bare:)
|
|
19
19
|
return '' if headings.empty?
|
|
20
20
|
|
|
@@ -45,7 +45,7 @@ module Raindown
|
|
|
45
45
|
|
|
46
46
|
# The document's headings, from its final HTML, or else by rendering it with its components.
|
|
47
47
|
def headings(markdown:, html: nil, metadata: nil, bare: false)
|
|
48
|
-
html ||= Fragments.collect { Raindown.assembled(markdown
|
|
48
|
+
html ||= Fragments.collect { Raindown.assembled(Context.new(document: markdown, metadata: metadata || NoMetadata, bare:)) }
|
|
49
49
|
|
|
50
50
|
html.gsub(IGNORED, '').scan(HEADING).map do |level, *ids, content|
|
|
51
51
|
{ level: level.to_i, text: CGI.unescapeHTML(content.gsub(TAG, '')).strip, id: CGI.unescapeHTML(ids.compact.first) }
|
data/lib/raindown/wiki_links.rb
CHANGED
|
@@ -5,7 +5,7 @@ require_relative 'front_matter'
|
|
|
5
5
|
|
|
6
6
|
module Raindown
|
|
7
7
|
# Obsidian's links between notes, which render as Markdown links to the file they name, by its path in the project,
|
|
8
|
-
# like
|
|
8
|
+
# like list: links, while the file keeps them as they are:
|
|
9
9
|
# [[Reading list]] [Reading list](<notes/Reading list.md>)
|
|
10
10
|
# [[Reading list|books]] [books](<notes/Reading list.md>)
|
|
11
11
|
# [[Reading list#To read]] [Reading list > To read](<notes/Reading list.md#to-read>)
|
data/lib/raindown.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative 'elements'
|
|
|
8
8
|
require_relative 'raindown/bare_directives'
|
|
9
9
|
require_relative 'raindown/code'
|
|
10
10
|
require_relative 'raindown/config'
|
|
11
|
+
require_relative 'raindown/context'
|
|
11
12
|
require_relative 'raindown/directives'
|
|
12
13
|
require_relative 'raindown/fragments'
|
|
13
14
|
require_relative 'raindown/front_matter'
|
|
@@ -32,7 +33,7 @@ module Raindown
|
|
|
32
33
|
# Without highlighting, code blocks are left for the browser to highlight, like <pre><code class="language-ruby">.
|
|
33
34
|
UNHIGHLIGHTED_OPTIONS = { render: { **HTML_OPTIONS[:render], github_pre_lang: false } }.freeze
|
|
34
35
|
|
|
35
|
-
# Stands in for an app's metadata when there isn't any, so list:
|
|
36
|
+
# Stands in for an app's metadata when there isn't any, so list: lists nothing.
|
|
36
37
|
module NoMetadata
|
|
37
38
|
def self.list(**) = []
|
|
38
39
|
end
|
|
@@ -49,13 +50,12 @@ module Raindown
|
|
|
49
50
|
# Bare directives like "toc:" show in bold in the HTML, and the rest stay hidden, as comments.
|
|
50
51
|
# Code blocks are highlighted, unless highlight_code is false for a site that highlights code in the browser.
|
|
51
52
|
def render(markdown:, metadata: NoMetadata, bare: false, highlight_code: true)
|
|
52
|
-
bare
|
|
53
|
+
context = Context.new(document: markdown, metadata:, bare: bare || BareDirectives.front_matter?(markdown))
|
|
53
54
|
|
|
54
55
|
highlighting(highlight_code) do
|
|
55
56
|
Fragments.collect do
|
|
56
57
|
# Components' HTML goes back in, then their headings get unique ids, before nodes like the TOC see it.
|
|
57
|
-
html = assembled(
|
|
58
|
-
html = Nodes.render_placeholders(html, document: markdown, metadata:, bare:, html:)
|
|
58
|
+
html = Nodes.render_placeholders(assembled(context), context)
|
|
59
59
|
|
|
60
60
|
Directives.render("#{html.chomp}\n")
|
|
61
61
|
end
|
|
@@ -77,15 +77,14 @@ module Raindown
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
# The document's HTML with its components' HTML put back, and unique heading ids.
|
|
80
|
-
def assembled(
|
|
81
|
-
HeadingIds.unique(Fragments.fill(html(
|
|
80
|
+
def assembled(context)
|
|
81
|
+
HeadingIds.unique(Fragments.fill(html(context)))
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
# The document's HTML from Commonmarker, after nodes and the template pass, with components as fragments.
|
|
85
|
-
def html(
|
|
86
|
-
markdown = NodeBlocks.to_rendered(
|
|
87
|
-
markdown
|
|
88
|
-
commonmark(PreRenderer.render(markdown, variables:, metadata:, bare:))
|
|
85
|
+
def html(context)
|
|
86
|
+
markdown = WikiLinks.to_markdown(NodeBlocks.to_rendered(context), metadata: context.metadata)
|
|
87
|
+
commonmark(PreRenderer.render(markdown, variables: context.variables, context:))
|
|
89
88
|
end
|
|
90
89
|
|
|
91
90
|
# Markdown to HTML, like a document's or a component's slot.
|
|
@@ -103,10 +102,6 @@ module Raindown
|
|
|
103
102
|
doc.to_html(options: UNHIGHLIGHTED_OPTIONS, plugins: { syntax_highlighter: nil })
|
|
104
103
|
end
|
|
105
104
|
|
|
106
|
-
def variables(markdown:)
|
|
107
|
-
Variables.new(FrontMatter.parse(markdown).first)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
105
|
# Commonmarker escapes directives, so they're hidden in HTML comments until it's done.
|
|
111
106
|
def hide_directives(markdown:)
|
|
112
107
|
markdown.gsub('<{', '<!-- ANTLERS').gsub('}>', 'ANTLERS -->').force_encoding('UTF-8')
|
data/lib/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: raindown
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- maedi
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-11-
|
|
10
|
+
date: 2026-11-06 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: antlers
|
|
@@ -204,10 +204,10 @@ files:
|
|
|
204
204
|
- bin/raindown
|
|
205
205
|
- lib/elements.rb
|
|
206
206
|
- lib/lexemes/endpoint_lexeme.rb
|
|
207
|
-
- lib/lexemes/
|
|
208
|
-
- lib/nodes/component_nodes.rb
|
|
207
|
+
- lib/lexemes/node_lexeme.rb
|
|
209
208
|
- lib/nodes/endpoint_node.rb
|
|
210
209
|
- lib/nodes/list_node.rb
|
|
210
|
+
- lib/nodes/write_in_node.rb
|
|
211
211
|
- lib/raindown.rb
|
|
212
212
|
- lib/raindown/actions.rb
|
|
213
213
|
- lib/raindown/bare_directives.rb
|
|
@@ -223,6 +223,7 @@ files:
|
|
|
223
223
|
- lib/raindown/code.rb
|
|
224
224
|
- lib/raindown/code_blocks.rb
|
|
225
225
|
- lib/raindown/config.rb
|
|
226
|
+
- lib/raindown/context.rb
|
|
226
227
|
- lib/raindown/directives.rb
|
|
227
228
|
- lib/raindown/document.rb
|
|
228
229
|
- lib/raindown/editing.rb
|
|
@@ -232,13 +233,13 @@ files:
|
|
|
232
233
|
- lib/raindown/endpoint/endpoint_runner.rb
|
|
233
234
|
- lib/raindown/endpoint/endpoint_variables.rb
|
|
234
235
|
- lib/raindown/endpoint/schema_block.rb
|
|
235
|
-
- lib/raindown/files.rb
|
|
236
236
|
- lib/raindown/fragments.rb
|
|
237
237
|
- lib/raindown/front_matter.rb
|
|
238
238
|
- lib/raindown/gitignore.rb
|
|
239
239
|
- lib/raindown/heading_ids.rb
|
|
240
240
|
- lib/raindown/js/host.mjs
|
|
241
241
|
- lib/raindown/js/js_nodes.rb
|
|
242
|
+
- lib/raindown/list.rb
|
|
242
243
|
- lib/raindown/markdown_nodes.rb
|
|
243
244
|
- lib/raindown/node.rb
|
|
244
245
|
- lib/raindown/node_blocks.rb
|
data/lib/lexemes/list_lexeme.rb
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'antlers/api'
|
|
4
|
-
|
|
5
|
-
module Raindown
|
|
6
|
-
module ListLexeme
|
|
7
|
-
include Antlers::Lexeme
|
|
8
|
-
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
KEYWORDS = ['list:', 'folder:', 'order:', ':list'].freeze
|
|
12
|
-
|
|
13
|
-
def match?(keywords:, **)
|
|
14
|
-
KEYWORDS.include?(keywords.keys.first)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def lexeme(keywords:, **)
|
|
18
|
-
if keywords['list:']
|
|
19
|
-
*key, value = keywords['list:'].split(',').map(&:strip)
|
|
20
|
-
|
|
21
|
-
list_def = { list_def: value, folder: keywords['folder:'] }
|
|
22
|
-
list_def[:order] = keywords['order:'] if keywords['order:']
|
|
23
|
-
list_def[:key] = key.first unless key.empty?
|
|
24
|
-
|
|
25
|
-
return list_def
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# TODO: Keep track of which list loop we're in to allow nested list loops.
|
|
29
|
-
{ list_end: 'level_1' }
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'antlers/api'
|
|
4
|
-
require 'nodes/html_node'
|
|
5
|
-
require 'nodes/prop_node'
|
|
6
|
-
require 'nodes/slot_node'
|
|
7
|
-
require_relative '../raindown/fragments'
|
|
8
|
-
|
|
9
|
-
module Raindown
|
|
10
|
-
# <{ Card name=p.name }>, rendered by Antlers where it is in the template, so its props see loop variables.
|
|
11
|
-
# Its HTML is kept as a fragment, so Commonmarker doesn't read it as Markdown.
|
|
12
|
-
class PropComponentNode < Antlers::PropNode
|
|
13
|
-
def render(**)
|
|
14
|
-
Fragments.add(super)
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# <{ Card: }>...<{ :Card }>, whose slot is its own Markdown, rendered to HTML in its scope first.
|
|
19
|
-
#
|
|
20
|
-
# Markdown children -> rendered to a Markdown string -> Commonmarker -> HTML
|
|
21
|
-
# -> wrapped as a single Antlers::HTMLNode (replaces @children for the super call)
|
|
22
|
-
# -> Antlers::SlotNode#render's "without a template" branch -> slot: receives that HTML
|
|
23
|
-
# -> Card#render(slot:, **props)
|
|
24
|
-
#
|
|
25
|
-
# This only reaches Card#render directly when Card has no class-level template (see SlotNode#render);
|
|
26
|
-
# otherwise Antlers renders Card's own template instead, with this HTML passed to it as the slot_node.
|
|
27
|
-
class SlotComponentNode < Antlers::SlotNode
|
|
28
|
-
def render(current_binding: nil, parent_binding: nil, slot_node: nil, metadata: {})
|
|
29
|
-
markdown = @children.map { |child| child.render(current_binding:, parent_binding:, slot_node:, metadata:) || '' }.join
|
|
30
|
-
children = @children
|
|
31
|
-
@children = [Antlers::HTMLNode.new(html: Raindown.commonmark(markdown))]
|
|
32
|
-
|
|
33
|
-
Fragments.add(super)
|
|
34
|
-
ensure
|
|
35
|
-
@children = children if children
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
data/lib/raindown/files.rb
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'node'
|
|
4
|
-
|
|
5
|
-
module Raindown
|
|
6
|
-
# The node behind files:, a list of links to the files the metadata has for its props, like files: folder: 'notes'.
|
|
7
|
-
# A project can change how files are listed with its own Files node in .raindown, like "class Files < Raindown::Files".
|
|
8
|
-
class Files < Node
|
|
9
|
-
def initialize(**tags)
|
|
10
|
-
@tags = tags
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Each file has its front matter, its path and its content.
|
|
14
|
-
def files(metadata: NoMetadata)
|
|
15
|
-
metadata.list(**@tags)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def replace(metadata: NoMetadata)
|
|
19
|
-
files(metadata:).map { |file| "- [#{file.title || file.path}](#{file.path})" }.join("\n")
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|