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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f795c35070ab8ae4f6b40e5a32595ab9ba6099b99f42aada8d0faafe2f56121d
4
- data.tar.gz: a01426b3aab916e21b0e68f66fddd7f6b55d7e9c34ceb9665118a60b3ceae4b5
3
+ metadata.gz: be9fc3af39c4d6f20b00b691a14582626448ac903a01aa129a2f33911553a76f
4
+ data.tar.gz: d0bf1eea7f8996adb29d97eeeaeda1c38ad16739df7724704ab1402b1bd2c723
5
5
  SHA512:
6
- metadata.gz: 052231532f48cc3a575388ebe95fa62b70bc3eba2121742142071c2ab256ced437de1f4c80f2e439599d579c2b5eea2d793b7cb05439601523bc72ff9c252dd6
7
- data.tar.gz: 98ff7b646f8e380817bbf69efc06a0bf67b4945a233349c9702d3140458bb27e387ecd010498c80dbfa4a9d2e64872b4f7af75bcd12b28b992e8b82ac66f56c9
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
- # Antlers' prop and slot directives, rendered as components in the template pass.
10
- plug(:prop_component) do
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
- require 'lexemes/prop_lexeme'
13
- Antlers::PropLexeme
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/component_nodes'
18
- PropComponentNode
18
+ require_relative 'nodes/list_node'
19
+ ListNode
19
20
  end
20
- end
21
21
 
22
- plug(:slot_component) do
23
- plug(:lexeme) do
24
- require 'lexemes/slot_lexeme'
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/component_nodes'
30
- SlotComponentNode
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
@@ -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
- include Antlers::Props
8
- include Antlers::Variables
13
+ DEF_KEY = :node_def
14
+ END_KEY = :node_end
9
15
 
10
- DEF_KEY = :list_def
11
- END_KEY = :list_end
16
+ def initialize(name:, args:)
17
+ super(name:)
12
18
 
13
- def initialize(value:, folder:, props: [], children: [])
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, parent_binding: nil, slot_node: nil, metadata: {})
21
- output = ''
22
-
23
- list = metadata.list(folder: @folder)
24
-
25
- list.each do |value|
26
- # TODO: Parallelize by creating new bindings and ensuring children have any args they need via RenderEvent.
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
- output
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(value: segment[:list_def], folder: segment[:folder])
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 files: are handled by NodeBlocks, before the rest.
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.
@@ -32,7 +32,7 @@ module Raindown
32
32
  end
33
33
 
34
34
  # The data is read every time, so the chart follows changes to its file.
35
- def replace(metadata: NoMetadata)
35
+ def rewrite(metadata: NoMetadata)
36
36
  points = points(metadata:)
37
37
  return '' if points.empty?
38
38
 
@@ -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
@@ -19,7 +19,7 @@ module Raindown
19
19
 
20
20
  module_function
21
21
 
22
- # A directive's line without its delimiters, like "files: folder: 'notes'" for <!-- files: folder: 'notes' -->.
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|
@@ -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
- "<!-- raindown:fragment #{list.length - 1} -->"
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.
@@ -16,13 +16,13 @@ const classes = {}
16
16
  const nodes = {}
17
17
 
18
18
  const requests = {
19
- // Every exported class with replace is a node, named by its export, or by its class for a default export.
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?.replace === 'function') classes[name === 'default' ? value.name : name] = value
25
+ if (typeof value?.prototype?.rewrite === 'function') classes[name === 'default' ? value.name : name] = value
26
26
  }
27
27
  }
28
28
 
@@ -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 replace and
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 replace and render are sent.
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 replace(**context)
76
- js_output(:replace, context)
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 replace(metadata: NoMetadata, bare: false)
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
- markdown = PreRenderer.render(template, variables: Variables.new(defaults.merge(@props)), metadata:, bare:)
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 Files.
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; end
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
@@ -16,9 +16,10 @@ module Raindown
16
16
  # - **1.2.0** Bare directives
17
17
  #
18
18
  # <!-- :Changelog -->
19
- # In Antlers' delimiters, <{ Changelog: }> to <{ :Changelog }>, or a self-closing <{ :Changelog }> or bare
20
- # :Changelog, it renders the node without writing it. toc: and files: are Raindown's own nodes. A name only starts
21
- # a block when it's a loaded node, so other text like "Note:" is left alone.
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
- SELF_CLOSING = /\A( {0,3})<\{\s*:(#{NAME})(?:\s+(.*?))?\s*\}>[ \t]*$/
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(lines, bare:).map do |span|
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 HTML, through a placeholder, or for its Markdown when it has no HTML of its own.
75
- # The directive and end tag become Directives markers, which also keep the node's output from joining
76
- # the Markdown around it, like two lists becoming one.
77
- def to_rendered(markdown, metadata: NoMetadata, bare: false)
78
- lines = markdown.lines
79
- find(lines, front_matter: FrontMatter.parse(markdown).first, bare:).reverse_each do |block|
80
- lines[block[:open]..block[:last]] = rendered_lines(lines, block, markdown:, metadata:, bare:)
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, markdown:, metadata:, bare:)
86
- output = node_markdown(block[:node], markdown:, metadata:, bare:).chomp
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(node, markdown:, metadata:, bare:)
93
- return Nodes.placeholder(node) if Nodes.html?(node)
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, :replace, document: markdown, metadata:, bare:).to_s
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, self_closing: true, **span }
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 files: folder: notes,
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], :replace, **context).to_s.chomp
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
 
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'chart/chart'
4
- require_relative 'files'
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
- # A node writes Markdown with #replace, and HTML with #render if it has one. Either can take the keywords
17
- # document: (the Markdown being rendered), metadata:, bare: and written: (the lines it wrote last time), and gets the
18
- # ones it asks for.
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, Files, Chart or Table node replaces Raindown's.
22
- BUILT_INS = { 'toc' => 'TOC', 'files' => 'Files', 'chart' => 'Chart', 'table' => 'Table' }.freeze
23
- # A placeholder for a node's HTML, which stays as it is through Commonmarker and Antlers.
24
- PLACEHOLDER = /<!-- raindown:node (\d+) -->/
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 replace, so text like "Note:" only starts a block when the Note node writes something.
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?(:replace)
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 replace or render with the context keywords it takes.
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
- def placeholder(node)
103
- "<!-- raindown:node #{list.index(node)} -->"
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
- # Swaps each placeholder for its node's HTML.
107
- def render_placeholders(html, **context)
108
- html.gsub(PLACEHOLDER) { html(list[::Regexp.last_match(1).to_i], **context) }
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 html(node, **context)
112
- return node.render_template(props: {}).to_s if node.class.respond_to?(:template) && node.class.template
126
+ def placeholder(node, slot: '')
127
+ return "<!-- raindown:node #{list.index(node)} -->" if slot.empty?
113
128
 
114
- call(node, :render, **context).to_s
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
- # Whether the node renders its own HTML rather than the HTML of its Markdown.
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
- node.respond_to?(:render) || (node.class.respond_to?(:template) && !node.class.template.nil?)
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
@@ -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: and files: list by tag.
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.file_types.values_at(*EXTENSIONS).flatten.compact
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 files: render before this. Components and endpoints render here too, as fragments of HTML
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
- BLOCK_LINE = /^[ \t]*(<\{\s*:?(?:#{BLOCK_DIRECTIVES.join('|')})\b[^}]*\}>)[ \t]*\n/
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:, metadata: {}, bare: false)
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, metadata)}"
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, metadata)
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
- # Components and endpoints render here too, where loop variables are in scope, and keep their HTML as fragments.
57
- elements = Antlers::Elements[:html, :var, :if, :for] + Elements[:list, :endpoint, :prop_component, :slot_component]
58
- rendered = Antlers.render(ast: Antlers.ast(template:, elements:), current_binding: variables.binding, metadata:)
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 replace(metadata: NoMetadata) = "```\n#{card_lines(metadata:).join("\n")}\n```"
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
@@ -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 replace(written: [], metadata: NoMetadata)
31
- path = File.expand_path(@data.to_s, metadata.respond_to?(:root) ? metadata.root : Dir.pwd)
32
- return "Can't show #{@data.inspect} as a table: only CSV and TSV files can be" unless SEPARATORS.key?(File.extname(path).downcase)
33
- return "Can't show #{@data.inspect} as a table: no such file" unless File.file?(path)
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
- synced(path, written.map(&:chomp))
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 replace(document:, metadata: nil, bare: false)
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:, metadata: metadata || NoMetadata, bare:) }
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) }
@@ -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 files: links, while the file keeps them as they are:
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: and files: list nothing.
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 ||= BareDirectives.front_matter?(markdown)
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(markdown:, metadata:, bare:)
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(markdown:, metadata: NoMetadata, bare: false)
81
- HeadingIds.unique(Fragments.fill(html(markdown:, variables: variables(markdown:), metadata:, bare:)))
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(markdown:, variables:, metadata: NoMetadata, bare: false)
86
- markdown = NodeBlocks.to_rendered(markdown.dup.force_encoding('UTF-8'), metadata:, bare:)
87
- markdown = WikiLinks.to_markdown(markdown, metadata:)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Raindown
4
- VERSION = '0.13.0'
4
+ VERSION = '0.14.0'
5
5
  end
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.13.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-05 00:00:00.000000000 Z
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/list_lexeme.rb
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
@@ -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
@@ -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