raindown 0.3.0 → 0.4.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: ee322a9f4df788bddfb51b6d669a7a676574620ecd1ce30e69a5682d49b8c88e
4
- data.tar.gz: 2bb5f698d9c5e90f0c44271e4b1529512dab858dcd07beb04061ac8690a81bff
3
+ metadata.gz: 17711b42b7bfd45766f0ad743b58035648b99394031bde5a420e92f01918f8a3
4
+ data.tar.gz: 94753448785a4825b3e2745d0690db75d69b052d57c3ca2e185793b427fedbef
5
5
  SHA512:
6
- metadata.gz: e550608449c87c693edef3c249472a89c62618de92941b81e135250462bbc74a062b61b2e025ea8666ff571b5057a25b67a13ed6fb385f304e500448f09a3034
7
- data.tar.gz: a60db4e479d593ea779e93c3c683401519599ac0bf1d07f6f1e2a280634eff26b5f46167f27d5ce24fcd08f54660cb4051ed21e0c422951247a8f3d2c53938a7
6
+ metadata.gz: be61212919255c9018253757f33d02e6cae3ec7811276791fe06bfbdaad748ffe4f47abeac8aad807d5862a7b2bcb493a2d53bebfd30f9abcb820c7faffaf438
7
+ data.tar.gz: 50488661cf504f7fa1fc9963a9aff7a6ebefb755ba46e2b0e63413133ee4e5781e9d9b94f0ba7c3965d7759b453d95ea7254713cf5e5dce73df2e6299de89b71
data/lib/raindown/cli.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
3
4
  require 'trees'
4
5
  require_relative '../raindown'
6
+ require_relative '../version'
5
7
  require_relative 'rewriter'
6
8
  require_relative 'watcher'
7
9
 
@@ -11,9 +13,13 @@ module Raindown
11
13
 
12
14
  USAGE = <<~TEXT
13
15
  Usage: raindown FILE [options]
16
+ raindown init [DIRECTORY]
14
17
 
15
18
  Renders FILE to HTML, then re-renders it every time it is saved.
16
19
 
20
+ Commands:
21
+ init [DIRECTORY] Create a .raindown folder for the project's config and nodes (default: current directory)
22
+
17
23
  Options:
18
24
  -o, --output PATH Write output to PATH instead of FILE.html beside FILE
19
25
  --once Render once without watching, printing to stdout unless -o is given
@@ -22,8 +28,21 @@ module Raindown
22
28
  --bare Allow bare directives like :toc in Markdown files
23
29
  -x, --execute Run endpoint curl snippets that have no output yet
24
30
  -h, --help Show this message
31
+ -v, --version Show the version
32
+
33
+ Set RAINDOWN_DEBUG=1 to show where an error happened.
25
34
  TEXT
26
35
 
36
+ CONFIG = <<~YAML
37
+ # Raindown project settings. See https://raindown.dev/docs/config
38
+ #
39
+ # Show directives as HTML comments rather than in bold:
40
+ # hide_directives: true
41
+ #
42
+ # Leave code blocks unhighlighted, so the browser can highlight them instead:
43
+ # highlight_code: false
44
+ YAML
45
+
27
46
  OPTIONS = { output: ['-o :output', '--output :output'], bare: ['--bare'], execute: ['-x', '--execute'],
28
47
  once: ['--once'], stdin: ['--stdin'] }.freeze
29
48
 
@@ -50,11 +69,34 @@ module Raindown
50
69
  end
51
70
  end
52
71
 
53
- # The :path line would match a help flag as the file, so help is checked first.
72
+ # The :path line would match help, version and init as the file, so they're checked first.
54
73
  def self.run(argv)
55
74
  abort(USAGE) if argv.empty? || argv.intersect?(%w[-h --help])
75
+ return puts(VERSION) if argv.intersect?(%w[-v --version])
76
+ return init(argv[1]) if argv.first == 'init' && argv.size <= 2
56
77
 
57
78
  super || abort("Unknown arguments: #{argv.join(' ')}\n\n#{USAGE}")
79
+ rescue StandardError => e
80
+ report(e, path: argv.first)
81
+ end
82
+
83
+ # Shows an error in a file as one line, rather than a backtrace, unless RAINDOWN_DEBUG is set.
84
+ def self.report(error, path:)
85
+ raise error if ENV['RAINDOWN_DEBUG']
86
+
87
+ abort("raindown: #{path}: #{error.message}")
88
+ end
89
+
90
+ # Creates the .raindown folder that marks a project's root and holds its config.yml and nodes.
91
+ def self.init(directory = nil)
92
+ folder = File.join(directory || Dir.pwd, Nodes::FOLDER)
93
+ config = File.join(folder, Config::FILE)
94
+ return puts("#{config} already exists") if File.exist?(config)
95
+
96
+ FileUtils.mkdir_p(folder)
97
+ File.write(config, CONFIG)
98
+ puts "Created #{config}"
99
+ puts 'Put your own nodes in the same folder. See https://raindown.dev/docs/nodes'
58
100
  end
59
101
 
60
102
  class << self
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'erb'
3
+ require 'cgi/escape'
4
4
 
5
5
  module Raindown
6
6
  # Content directives, like a node's directive and end tag or an endpoint's, are kept in the HTML:
@@ -22,7 +22,7 @@ module Raindown
22
22
  def render(html, hide: false)
23
23
  html.gsub(MARKER) do
24
24
  text = ::Regexp.last_match(1)
25
- hide ? "<!-- #{text} -->" : "<p><strong>#{ERB::Util.html_escape(text)}</strong></p>"
25
+ hide ? "<!-- #{text} -->" : "<p><strong>#{CGI.escapeHTML(text.to_s)}</strong></p>"
26
26
  end
27
27
  end
28
28
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'lowkey'
4
+
3
5
  require_relative 'code_blocks'
4
6
  require_relative 'directives'
5
7
  require_relative 'front_matter'
@@ -131,13 +133,12 @@ module Raindown
131
133
  text.scan(PROP).to_h { |name, value| [name.to_sym, value(value, variables)] }
132
134
  end
133
135
 
136
+ # A value, like 'text', 3, true, nil or :draft, or else a variable, like author.name.
134
137
  def value(value, variables)
135
- case value
136
- when /\A(["']).*\1\z/ then value[1..-2]
137
- when /\A-?\d+\z/ then value.to_i
138
- when /\A-?\d+\.\d+\z/ then value.to_f
139
- when 'true', 'false' then value == 'true'
140
- else Variables.value(variables, value)
138
+ Lowkey::ValueParser.parse(value) do
139
+ next value[1..-2] if value.match?(/\A(["']).*\1\z/)
140
+
141
+ Variables.value(variables, value)
141
142
  end
142
143
  end
143
144
  end
data/lib/raindown/toc.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'erb'
4
- require 'nokogiri'
3
+ require 'cgi/escape'
5
4
 
6
5
  require_relative 'node'
7
6
 
@@ -9,6 +8,12 @@ module Raindown
9
8
  # The node behind toc: and :toc, a table of contents of the document's headings.
10
9
  # A project can replace it with its own TOC node in .raindown, like "class TOC < Raindown::TOC".
11
10
  class TOC < Node
11
+ # A heading with an id, in the HTML that Commonmarker and Raindown's components write, like <h2 id="setup">.
12
+ HEADING = %r{<h([1-6])\b[^>]*?\bid=(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>(.*?)</h\1\s*>}mi
13
+ # Comments and scripts can hold heading tags that aren't in the page.
14
+ IGNORED = %r{<!--.*?-->|<script\b.*?</script\s*>}mi
15
+ TAG = /<[^>]*>/
16
+
12
17
  def replace(document:, metadata: nil, bare: false)
13
18
  headings = headings(markdown: document, metadata:, bare:)
14
19
  return '' if headings.empty?
@@ -23,7 +28,7 @@ module Raindown
23
28
  # With the document's final HTML, the headings include ones its components render.
24
29
  def render(document:, html: nil, metadata: nil, bare: false)
25
30
  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>"
31
+ "<li class='h#{heading[:level]}'><a href='##{heading[:id]}'>#{CGI.escapeHTML(heading[:text])}</a></li>"
27
32
  end
28
33
 
29
34
  <<~HTML
@@ -42,8 +47,8 @@ module Raindown
42
47
  def headings(markdown:, html: nil, metadata: nil, bare: false)
43
48
  html ||= Fragments.collect { Raindown.assembled(markdown:, metadata: metadata || NoMetadata, bare:) }
44
49
 
45
- Nokogiri::HTML(html).css(HEADINGS).map do |heading|
46
- { level: heading.name[1].to_i, text: heading.text.strip, id: heading['id'] }
50
+ html.gsub(IGNORED, '').scan(HEADING).map do |level, *ids, content|
51
+ { level: level.to_i, text: CGI.unescapeHTML(content.gsub(TAG, '')).strip, id: CGI.unescapeHTML(ids.compact.first) }
47
52
  end
48
53
  end
49
54
  end
@@ -19,8 +19,10 @@ module Raindown
19
19
 
20
20
  $stdout.sync = true
21
21
 
22
- rewrite
23
- render
22
+ safely do
23
+ rewrite
24
+ render
25
+ end
24
26
  puts "Watching #{path} for changes (writing to #{output})..."
25
27
 
26
28
  listener.start
@@ -35,14 +37,26 @@ module Raindown
35
37
 
36
38
  Listen.to(directory) do |modified, added, _removed|
37
39
  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
40
 
41
- render
42
- puts "Re-rendered #{path} -> #{output}"
41
+ safely do
42
+ # Formatting saves the file again, and that change triggers the render.
43
+ next if rewrite
44
+
45
+ render
46
+ puts "Re-rendered #{path} -> #{output}"
47
+ end
43
48
  end
44
49
  end
45
50
 
51
+ # An error in the file is shown rather than stopping the watcher, so it can be fixed and saved again.
52
+ def safely
53
+ yield
54
+ rescue StandardError => e
55
+ raise if ENV['RAINDOWN_DEBUG']
56
+
57
+ warn "raindown: #{path}: #{e.message}"
58
+ end
59
+
46
60
  # The files' metadata is loaded again each time, so new and renamed files are listed.
47
61
  def rewrite
48
62
  Rewriter.update(path:, bare:, execute:, metadata: Pages.for(path))
data/lib/raindown.rb CHANGED
@@ -35,9 +35,6 @@ module Raindown
35
35
  def self.list(**) = []
36
36
  end
37
37
 
38
- # Headings with an id, which are the ones a table of contents links to.
39
- HEADINGS = 'h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]'
40
-
41
38
  module_function
42
39
 
43
40
  # Directives show in bold and code is highlighted, unless the file's project changes them in .raindown/config.yml.
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Raindown
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-20 00:00:00.000000000 Z
10
+ date: 2026-10-25 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: antlers
@@ -80,21 +80,21 @@ dependencies:
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0'
82
82
  - !ruby/object:Gem::Dependency
83
- name: lownode
83
+ name: lowkey
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
- version: '0'
88
+ version: 0.7.0
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
- version: '0'
95
+ version: 0.7.0
96
96
  - !ruby/object:Gem::Dependency
97
- name: nokogiri
97
+ name: lownode
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - ">="