raindown 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'front_matter'
4
+ require_relative 'page'
5
+
6
+ module Raindown
7
+ # The metadata of a project's Markdown files, which list: and files: list by tag.
8
+ # Ported from Raindeer's Rain::Pages, and takes the same Dirload metadata.
9
+ #
10
+ # Each file is tagged with its front matter, like "tags: [ruby, docs]" or "date: 2099", and by its path,
11
+ # where underscored folders are metadata: docs/_basics/_1-getting-started.md has
12
+ # folder: basics, order: 1, path: docs/_basics/_1-getting-started.md
13
+ # Folder tags can share a folder name with "&", like docs/_1&_basics/getting-started.md.
14
+ # An app can add its own metadata by subclassing and overriding implicit_metadata, like Raindeer's URL paths.
15
+ class Pages
16
+ EXTENSIONS = %w[md rd markdown raindown].freeze
17
+ SEGMENTS = Regexp.union(['/', '&', '-'])
18
+
19
+ class << self
20
+ # The Markdown files under a project's root, the folder with its .raindown folder or else the file's folder.
21
+ def for(path)
22
+ file_directory = File.dirname(File.expand_path(path))
23
+ load(root: Nodes.find_folder(file_directory)&.then { File.dirname(it) } || file_directory)
24
+ end
25
+
26
+ # Lists the files under the root with Dirload, without loading them.
27
+ def load(root:)
28
+ require 'dirload'
29
+ file_types = Dirload.file_paths(absolute_path: root).group_by { Dirload.extension(file_path: it) }
30
+ new(metadata: Dirload::Metadata.new(loaded_paths: {}, missed_paths: [], file_types:), root:)
31
+ end
32
+ end
33
+
34
+ # Render options, like hide_directives: true, are for rendering each listed file's content.
35
+ def initialize(metadata:, root:, render_options: {})
36
+ @root = root
37
+ @render_options = render_options
38
+ @file_paths = metadata.file_types.values_at(*EXTENSIONS).flatten.compact
39
+ @metadata = {}
40
+ @tags = {}
41
+
42
+ @file_paths.each { |file_path| process(file_path:) }
43
+ end
44
+
45
+ # The files with any of the tags, like list(folder: 'basics') or list(tags: 'ruby'), sorted by order.
46
+ # Without tags, every file.
47
+ def list(**tags)
48
+ file_paths = tags.empty? ? @file_paths : tagged(**tags)
49
+ file_paths.sort_by { |file_path| sort_key(file_path) }.map { |file_path| listed_page(file_path) }
50
+ end
51
+
52
+ def tagged(**tags)
53
+ tags.flat_map { |type, tag| @tags.dig(type.to_sym, tag) || [] }.uniq
54
+ end
55
+
56
+ private
57
+
58
+ def listed_page(file_path)
59
+ Page.new(file_path:, metadata: @metadata[file_path], render_options: @render_options)
60
+ end
61
+
62
+ def process(file_path:)
63
+ metadata = implicit_metadata(file_path:).merge(FrontMatter.parse(File.read(file_path)).first)
64
+ @metadata[file_path] = metadata
65
+ metadata.each { |type, tag| Array(tag).each { |item| tag(type:, tag: item, file_path:) } }
66
+ end
67
+
68
+ # The path is relative to the root, like docs/_basics/_1-getting-started.md.
69
+ # A subclass can add or replace metadata here, like Raindeer's Rain::Pages replacing the path with a URL path.
70
+ def implicit_metadata(file_path:)
71
+ { folder: folders(file_path:).last, order: order(file_path:), path: relative(file_path).delete_prefix('/') }.compact
72
+ end
73
+
74
+ def tag(type:, tag:, file_path:)
75
+ @tags[type] ||= {}
76
+ @tags[type][tag] ||= []
77
+ @tags[type][tag] << file_path
78
+ end
79
+
80
+ # The underscored folders, like basics in docs/_basics/.
81
+ def folders(file_path:)
82
+ underscored(File.dirname(relative(file_path))).reject { number?(it) }
83
+ end
84
+
85
+ # The last underscored number in the path, like 1 in docs/_basics/_1-getting-started.md.
86
+ def order(file_path:)
87
+ underscored(relative(file_path)).select { number?(it) }.last&.then { Integer(it, exception: false) || Float(it) }
88
+ end
89
+
90
+ def underscored(path)
91
+ path.split(SEGMENTS).select { it.start_with?('_') }.map { it.delete_prefix('_') }
92
+ end
93
+
94
+ def relative(file_path)
95
+ File.expand_path(file_path).delete_prefix(File.expand_path(@root))
96
+ end
97
+
98
+ def number?(string)
99
+ !Float(string, exception: false).nil?
100
+ end
101
+
102
+ # Files with an order come first, by order, then the rest by path.
103
+ def sort_key(file_path)
104
+ order = @metadata[file_path][:order]
105
+ [order.is_a?(Numeric) ? 0 : 1, order.is_a?(Numeric) ? order : 0, @metadata[file_path][:path].to_s]
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'antlers'
4
+ require_relative '../elements'
5
+ require_relative 'bare_directives'
6
+ require_relative 'code'
7
+ require_relative 'front_matter'
8
+
9
+ module Raindown
10
+ # Renders the directives whose output is Markdown, before Commonmarker reads it: variables, loops, conditions and lists.
11
+ # So a loop's items become one Markdown list or table, and a heading's id and its entry in a table of contents
12
+ # come from its text, like "# Notes for {name}" becoming "Notes for Mark" with the id "notes-for-mark".
13
+ # Nodes like toc: and files: render before this. Components and endpoints render here too, as fragments of HTML
14
+ # that Raindown puts back once the document is HTML.
15
+ module PreRenderer
16
+ BLOCK_DIRECTIVES = %w[for if list].freeze
17
+ # A for, if or list directive alone on its line, which leaves no line behind, so a loop's list items stay together.
18
+ BLOCK_LINE = /^[ \t]*(<\{\s*:?(?:#{BLOCK_DIRECTIVES.join('|')})\b[^}]*\}>)[ \t]*\n/
19
+
20
+ module_function
21
+
22
+ def render(markdown, variables:, metadata: {}, bare: false)
23
+ markdown = BareDirectives.expand(markdown) if bare
24
+ return markdown unless markdown.include?('{')
25
+
26
+ markdown = Code.hide_braces(markdown.dup.force_encoding('UTF-8'))
27
+ body = FrontMatter.parse(markdown).last
28
+ "#{markdown.delete_suffix(body)}#{render_body(body, variables, metadata)}"
29
+ end
30
+
31
+ def render_body(body, variables, metadata)
32
+ template = body.gsub(BLOCK_LINE, '\1')
33
+ return body unless template.include?('{')
34
+
35
+ # Antlers strips the whitespace around a template, which can be Markdown, like an indented code block.
36
+ leading = template[/\A\s*/]
37
+ trailing = template[/\s*\z/]
38
+ # Components and endpoints render here too, where loop variables are in scope, and keep their HTML as fragments.
39
+ elements = Antlers::Elements[:html, :var, :if, :for] + Elements[:list, :endpoint, :prop_component, :slot_component]
40
+ rendered = Antlers.render(ast: Antlers.ast(template:, elements:), current_binding: variables.binding, metadata:)
41
+
42
+ "#{leading}#{rendered}#{trailing}"
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'node_injector'
4
+ require_relative 'endpoint/endpoint_runner'
5
+ require_relative 'table_formatter'
6
+
7
+ module Raindown
8
+ # Rewrites the generated parts of a Markdown file in place before it's rendered:
9
+ # node output, like the TOC, endpoint responses and table alignment.
10
+ # Each rewriter responds to rewrite(content:, path:, bare:, execute:, metadata:) and returns the new content.
11
+ module Rewriter
12
+ REWRITERS = [NodeInjector, EndpointRunner, TableFormatter].freeze
13
+
14
+ module_function
15
+
16
+ def rewrite(content:, path:, bare: false, execute: false, metadata: NoMetadata)
17
+ REWRITERS.reduce(content) do |rewritten, rewriter|
18
+ rewriter.rewrite(content: rewritten, path:, bare:, execute:, metadata:)
19
+ end
20
+ end
21
+
22
+ # Returns true when the file changed.
23
+ def update(path:, bare: false, execute: false, metadata: NoMetadata)
24
+ content = File.read(path)
25
+ rewritten = rewrite(content:, path:, bare:, execute:, metadata:)
26
+ return false if rewritten == content
27
+
28
+ File.write(path, rewritten)
29
+ true
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'unicode/display_width'
4
+
5
+ module Raindown
6
+ # Pads every column of each Markdown table to the width of its widest cell.
7
+ class TableFormatter
8
+ DELIMITER_CELL = /\A:?-+:?\z/
9
+ FENCE = /\A\s*(```|~~~)/
10
+
11
+ def self.rewrite(content:, **)
12
+ new(content:).format
13
+ end
14
+
15
+ def initialize(content:)
16
+ @lines = content.lines
17
+ end
18
+
19
+ def format
20
+ output = []
21
+ index = 0
22
+ in_fence = false
23
+
24
+ while index < @lines.length
25
+ line = @lines[index]
26
+ in_fence = !in_fence if line.match?(FENCE)
27
+
28
+ if !in_fence && table_start?(index)
29
+ table_end = find_table_end(index)
30
+ output.concat(align(@lines[index...table_end]))
31
+ index = table_end
32
+ else
33
+ output << line
34
+ index += 1
35
+ end
36
+ end
37
+
38
+ output.join
39
+ end
40
+
41
+ private
42
+
43
+ def table_start?(index)
44
+ header = @lines[index]
45
+ delimiter = @lines[index + 1]
46
+ return false unless header&.include?('|') && delimiter
47
+
48
+ delimiter_cells = cells(delimiter)
49
+ delimiter_cells.any? && delimiter_cells.all? { |cell| cell.match?(DELIMITER_CELL) } &&
50
+ delimiter_cells.length == cells(header).length
51
+ end
52
+
53
+ def find_table_end(index)
54
+ table_end = index + 2
55
+ table_end += 1 while @lines[table_end]&.include?('|') && !@lines[table_end].strip.empty?
56
+ table_end
57
+ end
58
+
59
+ def align(table)
60
+ indent = table.first[/\A\s*/]
61
+ rows = table.map { |line| cells(line) }
62
+ header, delimiter, *body = rows
63
+ return table if body.any? { |row| row.length > header.length }
64
+
65
+ table_lines(indent:, header:, body:, alignments: delimiter.map { |cell| alignment(cell) })
66
+ end
67
+
68
+ def table_lines(indent:, header:, body:, alignments:)
69
+ body = body.map { |row| row + ([''] * (header.length - row.length)) }
70
+ widths = column_widths(rows: [header, *body])
71
+
72
+ [
73
+ row_line(indent:, cells: header, widths:, alignments:),
74
+ delimiter_line(indent:, widths:, alignments:),
75
+ *body.map { |row| row_line(indent:, cells: row, widths:, alignments:) }
76
+ ].map { |line| "#{line}\n" }
77
+ end
78
+
79
+ # Split on pipes that aren't escaped, dropping the optional outer pipes.
80
+ def cells(line)
81
+ content = line.strip
82
+ content = content.delete_prefix('|')
83
+ content = content.delete_suffix('|') unless content.end_with?('\|')
84
+
85
+ content.split(/(?<!\\)\|/, -1).map(&:strip)
86
+ end
87
+
88
+ def alignment(cell)
89
+ if cell.start_with?(':') && cell.end_with?(':') then :center
90
+ elsif cell.end_with?(':') then :right
91
+ elsif cell.start_with?(':') then :left
92
+ end
93
+ end
94
+
95
+ # Three is the shortest delimiter cell, which also fits ':-:'.
96
+ def column_widths(rows:)
97
+ rows.transpose.map { |column| [column.map { |cell| display_width(cell) }.max, 3].max }
98
+ end
99
+
100
+ # Wide characters like CJK and emoji take two columns. Emoji are always measured as two,
101
+ # instead of guessing from the terminal, so a file formats the same on every machine.
102
+ def display_width(cell)
103
+ Unicode::DisplayWidth.of(cell, emoji: :all)
104
+ end
105
+
106
+ def row_line(indent:, cells:, widths:, alignments:)
107
+ padded = cells.each_with_index.map { |cell, i| pad(cell, widths[i], alignments[i]) }
108
+ "#{indent}| #{padded.join(' | ')} |"
109
+ end
110
+
111
+ def delimiter_line(indent:, widths:, alignments:)
112
+ dashes = widths.each_with_index.map do |width, i|
113
+ case alignments[i]
114
+ when :center then ":#{'-' * (width - 2)}:"
115
+ when :right then "#{'-' * (width - 1)}:"
116
+ when :left then ":#{'-' * (width - 1)}"
117
+ else '-' * width
118
+ end
119
+ end
120
+
121
+ "#{indent}| #{dashes.join(' | ')} |"
122
+ end
123
+
124
+ def pad(cell, width, alignment)
125
+ padding = width - display_width(cell)
126
+
127
+ case alignment
128
+ when :center then "#{' ' * (padding / 2)}#{cell}#{' ' * (padding - (padding / 2))}"
129
+ when :right then "#{' ' * padding}#{cell}"
130
+ else "#{cell}#{' ' * padding}"
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'nokogiri'
5
+
6
+ require_relative 'node'
7
+
8
+ module Raindown
9
+ # The node behind toc: and :toc, a table of contents of the document's headings.
10
+ # A project can replace it with its own TOC node in .raindown, like "class TOC < Raindown::TOC".
11
+ class TOC < Node
12
+ def replace(document:, metadata: nil, bare: false)
13
+ headings = headings(markdown: document, metadata:, bare:)
14
+ return '' if headings.empty?
15
+
16
+ min_level = headings.map { |heading| heading[:level] }.min
17
+
18
+ headings.map do |heading|
19
+ "#{' ' * (heading[:level] - min_level)}- [#{heading[:text]}](##{heading[:id]})"
20
+ end.join("\n")
21
+ end
22
+
23
+ # With the document's final HTML, the headings include ones its components render.
24
+ def render(document:, html: nil, metadata: nil, bare: false)
25
+ items = headings(markdown: document, html:, metadata:, bare:).map do |heading|
26
+ "<li class='h#{heading[:level]}'><a href='##{heading[:id]}'>#{ERB::Util.html_escape(heading[:text])}</a></li>"
27
+ end
28
+
29
+ <<~HTML
30
+ <div class="floating">
31
+ <details id="toc" open>
32
+ <summary>Table of contents</summary>
33
+ <ul>
34
+ #{items.join("\n")}
35
+ </ul>
36
+ </details>
37
+ </div>
38
+ HTML
39
+ end
40
+
41
+ # The document's headings, from its final HTML, or else by rendering it with its components.
42
+ def headings(markdown:, html: nil, metadata: nil, bare: false)
43
+ html ||= Fragments.collect { Raindown.assembled(markdown:, metadata: metadata || NoMetadata, bare:) }
44
+
45
+ Nokogiri::HTML(html).css(HEADINGS).map do |heading|
46
+ { level: heading.name[1].to_i, text: heading.text.strip, id: heading['id'] }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raindown
4
+ # The front matter values that Antlers variables like {title} and <{ title }> render.
5
+ # Antlers reads a variable from the binding's locals, then by sending it to the binding's receiver, which is this.
6
+ # It's a BasicObject so a key like "hash" or "display" isn't shadowed by a method, and unknown names render nothing.
7
+ # Nested values work too, like {author.name} and <{ for: tag in: tags }>.
8
+ class Variables < BasicObject
9
+ # Braces in a value become the placeholders that render as braces, so Antlers doesn't read them as a variable.
10
+ def self.wrap(value)
11
+ case value
12
+ when ::Hash then new(value)
13
+ when ::Array then value.map { |item| wrap(item) }
14
+ when ::String then value.gsub(/[{}]/, CODE_BRACES)
15
+ else value
16
+ end
17
+ end
18
+
19
+ def initialize(values)
20
+ @values = values.to_h { |key, value| [key.to_sym, Variables.wrap(value)] }
21
+ end
22
+
23
+ def respond_to?(name, _include_all = false) # rubocop:disable Style/OptionalBooleanParameter -- Ruby's signature
24
+ @values.key?(name.to_sym)
25
+ end
26
+
27
+ def send(name, *)
28
+ @values[name.to_sym]
29
+ end
30
+
31
+ def binding
32
+ ::Kernel.binding
33
+ end
34
+
35
+ def to_s
36
+ @values.to_s
37
+ end
38
+ alias inspect to_s
39
+
40
+ def method_missing(*)
41
+ nil
42
+ end
43
+
44
+ def respond_to_missing?(*)
45
+ false
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../raindown'
4
+ require_relative 'rewriter'
5
+
6
+ module Raindown
7
+ class Watcher
8
+ attr_reader :path, :output, :bare, :execute
9
+
10
+ def initialize(path:, output: nil, bare: false, execute: false)
11
+ @path = path
12
+ @output = output || default_output
13
+ @bare = bare
14
+ @execute = execute
15
+ end
16
+
17
+ def start
18
+ require 'listen'
19
+
20
+ $stdout.sync = true
21
+
22
+ rewrite
23
+ render
24
+ puts "Watching #{path} for changes (writing to #{output})..."
25
+
26
+ listener.start
27
+ sleep
28
+ end
29
+
30
+ private
31
+
32
+ def listener
33
+ directory = File.dirname(File.expand_path(path))
34
+ filename = File.basename(path)
35
+
36
+ Listen.to(directory) do |modified, added, _removed|
37
+ next unless (modified + added).any? { |f| File.basename(f) == filename }
38
+ # Formatting saves the file again, and that change triggers the render.
39
+ next if rewrite
40
+
41
+ render
42
+ puts "Re-rendered #{path} -> #{output}"
43
+ end
44
+ end
45
+
46
+ # The files' metadata is loaded again each time, so new and renamed files are listed.
47
+ def rewrite
48
+ Rewriter.update(path:, bare:, execute:, metadata: Pages.for(path))
49
+ end
50
+
51
+ def render
52
+ File.write(output, Raindown.render_file(path:, bare:, metadata: Pages.for(path)))
53
+ end
54
+
55
+ def default_output
56
+ "#{File.dirname(path)}/#{File.basename(path, '.*')}.html"
57
+ end
58
+ end
59
+ end
data/lib/raindown.rb CHANGED
@@ -1,31 +1,96 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'commonmarker'
4
+ require 'antlers'
4
5
  require 'antlers/api'
6
+
5
7
  require_relative 'elements'
8
+ require_relative 'raindown/bare_directives'
9
+ require_relative 'raindown/code'
10
+ require_relative 'raindown/config'
11
+ require_relative 'raindown/directives'
12
+ require_relative 'raindown/fragments'
13
+ require_relative 'raindown/front_matter'
14
+ require_relative 'raindown/heading_ids'
15
+ require_relative 'raindown/node_blocks'
16
+ require_relative 'raindown/pages'
17
+ require_relative 'raindown/pre_renderer'
18
+ require_relative 'raindown/variables'
19
+
20
+ module Raindown
21
+ # Antlers evaluates any braces in the HTML, so braces in code are swapped for placeholders
22
+ # then HTML entities, which browsers still display as braces.
23
+ CODE_BRACES = { '{' => "\u{E000}", '}' => "\u{E001}" }.freeze
24
+ CODE_BRACE_ENTITIES = { "\u{E000}" => '&#123;', "\u{E001}" => '&#125;' }.freeze
25
+
26
+ # Front matter is metadata, so it's left out of the HTML and the headings.
27
+ MARKDOWN_OPTIONS = { extension: { alerts: true, front_matter_delimiter: '---' } }.freeze
28
+ # Directives are HTML comments until Antlers renders them, and escaped characters like "\#" stay plain text.
29
+ HTML_OPTIONS = { render: { unsafe: true, escaped_char_spans: false } }.freeze
30
+
31
+ # Stands in for an app's metadata when there isn't any, so list: and files: list nothing.
32
+ module NoMetadata
33
+ def self.list(**) = []
34
+ end
6
35
 
7
- module Rain
8
- module Raindown
9
- module_function
36
+ # Headings with an id, which are the ones a table of contents links to.
37
+ HEADINGS = 'h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]'
10
38
 
11
- def render(markdown:, metadata: {})
12
- template = markdown.gsub('<{', '<!-- ANTLERS').gsub('}>', 'ANTLERS -->')
39
+ module_function
13
40
 
14
- doc = Commonmarker.parse(template.force_encoding('UTF-8'), options: { extension: { alerts: true } })
15
- doc.walk do |node|
16
- if %i[code code_block].include?(node.type)
17
- node.string_content = node.string_content.gsub('<!-- ANTLERS', '<{').gsub('ANTLERS -->', '}>')
18
- end
19
- end
41
+ # Directives show in bold, unless the file's project hides them in .raindown/config.yml.
42
+ def render_file(path:, metadata: NoMetadata, bare: false, hide_directives: Config.for(path)[:hide_directives])
43
+ render(markdown: File.read(path), metadata:, bare: bare || BareDirectives.file?(path), hide_directives:)
44
+ end
20
45
 
21
- template = doc.to_html(options: { render: { unsafe: true } })
22
- template = template.gsub('<!-- ANTLERS', '<{').gsub('ANTLERS -->', '}>')
46
+ # Front matter keys are variables, like {title} or <{ title }>.
47
+ # Directives like "toc:" export to HTML as bold or comments depending on hide_directives config.
48
+ def render(markdown:, metadata: NoMetadata, bare: false, hide_directives: false)
49
+ Fragments.collect do
50
+ # Components' HTML goes back in, then their headings get unique ids, before nodes like the TOC see it.
51
+ html = assembled(markdown:, metadata:, bare:)
52
+ html = Nodes.render_placeholders(html, document: markdown, metadata:, bare:, html:)
23
53
 
24
- return template unless template.include?('<{') || template.include?('{')
54
+ Directives.render("#{html.chomp}\n", hide: hide_directives)
55
+ end
56
+ end
57
+
58
+ # The document's HTML with its components' HTML put back, and unique heading ids.
59
+ def assembled(markdown:, metadata: NoMetadata, bare: false)
60
+ HeadingIds.unique(Fragments.fill(html(markdown:, variables: variables(markdown:), metadata:, bare:)))
61
+ end
62
+
63
+ # The document's HTML from Commonmarker, after nodes and the template pass, with components as fragments.
64
+ def html(markdown:, variables:, metadata: NoMetadata, bare: false)
65
+ markdown = NodeBlocks.to_rendered(markdown.dup.force_encoding('UTF-8'), metadata:, bare:)
66
+ commonmark(PreRenderer.render(markdown, variables:, metadata:, bare:))
67
+ end
68
+
69
+ # Markdown to HTML, like a document's or a component's slot.
70
+ def commonmark(markdown)
71
+ doc = Commonmarker.parse(hide_directives(markdown:), options: MARKDOWN_OPTIONS)
72
+ preserve_code(doc:)
73
+
74
+ doc.to_html(options: HTML_OPTIONS).gsub('<!-- ANTLERS', '<{').gsub('ANTLERS -->', '}>')
75
+ .gsub(/[\u{E000}\u{E001}]/, CODE_BRACE_ENTITIES)
76
+ end
77
+
78
+ def variables(markdown:)
79
+ Variables.new(FrontMatter.parse(markdown).first)
80
+ end
81
+
82
+ # Commonmarker escapes directives, so they're hidden in HTML comments until it's done.
83
+ def hide_directives(markdown:)
84
+ markdown.gsub('<{', '<!-- ANTLERS').gsub('}>', 'ANTLERS -->').force_encoding('UTF-8')
85
+ end
25
86
 
26
- ast = Antlers.ast(template:, elements: Antlers::Elements[:html, :var, :prop, :slot, :if, :for] + Rain::Elements[:toc, :list])
87
+ # Restore directives in code and hide its braces from Antlers.
88
+ def preserve_code(doc:)
89
+ doc.walk do |node|
90
+ next unless %i[code code_block].include?(node.type)
27
91
 
28
- Antlers.render(ast:, current_binding: binding, metadata:)
92
+ node.string_content = node.string_content.gsub('<!-- ANTLERS', '<{').gsub('ANTLERS -->', '}>')
93
+ .gsub(/[{}]/, CODE_BRACES)
29
94
  end
30
95
  end
31
96
  end
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Raindown
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.1'
5
5
  end