jekyll-carve 0.1.0 → 0.1.2
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/README.md +129 -9
- data/lib/jekyll/carve/version.rb +1 -1
- data/lib/jekyll-carve.rb +313 -4
- metadata +10 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1fd1b876054b8355dc08cdf7569c46cafd968c9e8874c65cd4e6e2866f29d6e7
|
|
4
|
+
data.tar.gz: c4866d9b82ff1d2bf963eb8945fa4a67b70b94aaa4d3e78b300229d369cefc4d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b0c225c70722c0ed014f0ade97aae1b534992dd6830b02acf47a27b1914fe92208f478aefb72ec071794be0fd9e33f75be25a365fe55ef5f3b7be0b8e23b1f0a
|
|
7
|
+
data.tar.gz: 6c86ca36aec167aa996d1b01df87113e9e0ef8796f97a0cde5c4a1e4f6badc6b0edbad271886c7cd192a7e1167f7c23042952e74641907cae0e1a87f7e65f2e2
|
data/README.md
CHANGED
|
@@ -51,6 +51,131 @@ hyphenated/underscored forms, passed straight through to the engine). When the
|
|
|
51
51
|
key is absent, no extensions are enabled. The recognized extensions are listed
|
|
52
52
|
in `Carve::EXTENSIONS`; an unknown name raises `ArgumentError` at build time.
|
|
53
53
|
|
|
54
|
+
### `carve.symbols` - what `:smile:` renders as
|
|
55
|
+
|
|
56
|
+
Carve parses `:name:` in core, no extension needed, but what a name renders as
|
|
57
|
+
is a render option. With no map configured a shortcode renders as its own
|
|
58
|
+
source text:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
Ship it :smile:
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<p>Ship it :smile:</p>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`carve.symbols` supplies the map. It takes a mapping written inline:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
carve:
|
|
72
|
+
symbols:
|
|
73
|
+
smile: "😄"
|
|
74
|
+
ship: "🚀"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
or a path to a JSON object, so a large map does not have to live in
|
|
78
|
+
`_config.yml`:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
carve:
|
|
82
|
+
symbols: _data/symbols.json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"smile": "😄",
|
|
88
|
+
"ship": "🚀"
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
or a list mixing both, merged left to right, so a generated map can carry a few
|
|
93
|
+
site-specific overrides:
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
carve:
|
|
97
|
+
symbols:
|
|
98
|
+
- _data/emoji.json
|
|
99
|
+
- ship: "🚀"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A path is resolved against the site source and confined to it, symlinks
|
|
103
|
+
included - a link inside the source pointing at a file outside it is refused
|
|
104
|
+
rather than followed. A name that
|
|
105
|
+
is not in the map keeps rendering as its own text, and the map does not loosen
|
|
106
|
+
Carve's word-boundary rule: `10:30:` and `a:smile:b` are not shortcodes, and
|
|
107
|
+
`` `:smile:` `` inside a code span stays code.
|
|
108
|
+
|
|
109
|
+
No emoji table ships with this plugin. Jekyll has no emoji database in core, so
|
|
110
|
+
bundling one here would be a second source of truth next to whatever your
|
|
111
|
+
Markdown pages already use (`jemoji`, for instance) - and the two would drift.
|
|
112
|
+
Point `carve.symbols` at a JSON file you generate from that same source and
|
|
113
|
+
both page types resolve one map.
|
|
114
|
+
|
|
115
|
+
A misconfigured map fails the build rather than being warned past: a missing
|
|
116
|
+
file, invalid JSON, a JSON top level that is not an object, or a value that is
|
|
117
|
+
not a string each raise `ArgumentError` naming the file or the key. A value is
|
|
118
|
+
not coerced, because it reaches the page raw - `count: 1` is a mistake worth
|
|
119
|
+
stopping for, not something to stringify and emit. A NAME is coerced, since
|
|
120
|
+
YAML hands back a Symbol or a boolean for some unquoted keys and a name never
|
|
121
|
+
reaches the output.
|
|
122
|
+
|
|
123
|
+
> [!WARNING]
|
|
124
|
+
> A symbol value is inserted as **trusted raw output**. It is not escaped, so
|
|
125
|
+
> `smile: "<img src='/s.svg'>"` emits a real `<img>` element rather than
|
|
126
|
+
> escaped text. This is deliberate in the engine - `carve-lang` documents it as
|
|
127
|
+
> "NEVER build a symbols map out of untrusted / user-supplied input" - and it
|
|
128
|
+
> is why this plugin reads the map only from `_config.yml` and from files at
|
|
129
|
+
> paths named there. Never generate `carve.symbols` from page content, from
|
|
130
|
+
> front matter, from a comment system, or from anything else a visitor can
|
|
131
|
+
> influence.
|
|
132
|
+
|
|
133
|
+
### `carve.includes` - pulling one file into another
|
|
134
|
+
|
|
135
|
+
A Carve page can pull another file in with `{{ path }}`. It stays literal until
|
|
136
|
+
the site asks:
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
carve:
|
|
140
|
+
includes: true
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Includes need carve-lang 0.1.4 or later. On an older engine, a page that
|
|
144
|
+
reaches them stops the build with a message naming the upgrade.
|
|
145
|
+
|
|
146
|
+
Paths resolve relative to the file that wrote them, and nothing resolves outside
|
|
147
|
+
the containment root. The root is Jekyll's source directory unless
|
|
148
|
+
`carve.include_root` names another one, and an include that would leave it is
|
|
149
|
+
not expanded.
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
carve:
|
|
153
|
+
includes: true
|
|
154
|
+
include_root: /srv/shared/fragments
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`carve.include_root` must be an **absolute** path. A relative one is refused
|
|
158
|
+
rather than resolved, because resolving it lands on whatever directory the build
|
|
159
|
+
ran from, which is not a root anyone chose. The page being rendered has to sit
|
|
160
|
+
inside the root too.
|
|
161
|
+
|
|
162
|
+
A target that cannot be read is reported against the page that asked for it, and
|
|
163
|
+
the directive is left as written. The message does not say whether the file was
|
|
164
|
+
missing or refused by containment: both report `include-unresolved`, so a page
|
|
165
|
+
cannot be used to probe the filesystem. The class is at `--verbose`.
|
|
166
|
+
|
|
167
|
+
Under `jekyll serve`, a page is rebuilt when a target it read changes, because
|
|
168
|
+
every resolved target is registered with Jekyll's regeneration path.
|
|
169
|
+
|
|
170
|
+
A conversion with no page behind it - the converter called directly on a String -
|
|
171
|
+
leaves the directive literal, since there is no file for a relative path to
|
|
172
|
+
resolve against.
|
|
173
|
+
|
|
174
|
+
> [!NOTE]
|
|
175
|
+
> Fragments under the source directory are pages in their own right, so Jekyll
|
|
176
|
+
> will publish them. Exclude them in `_config.yml`, or keep them outside the
|
|
177
|
+
> source and point `include_root` at the tree above both.
|
|
178
|
+
|
|
54
179
|
## Usage
|
|
55
180
|
|
|
56
181
|
Create a page with a `.crv` extension. It MUST begin with Jekyll
|
|
@@ -107,16 +232,11 @@ has already removed the leading block by the time Carve runs.
|
|
|
107
232
|
| ------ | -------- |
|
|
108
233
|
| `matches(ext)` | `true` for `.crv` (with or without the leading dot, case-insensitive), `false` otherwise. |
|
|
109
234
|
| `output_ext(ext)` | `".html"` (includes the dot so Jekyll emits `page.html`). |
|
|
110
|
-
| `convert(content)` | `Carve.to_html(content, extensions: configured)` - the rendered HTML. |
|
|
235
|
+
| `convert(content)` | `Carve.to_html(content, extensions: configured, symbols: configured)` - the rendered HTML. |
|
|
111
236
|
| `carve_extensions` | The extension list read from `carve.extensions` in `_config.yml` (empty by default). |
|
|
237
|
+
| `carve_symbols` | The `:name:` map read from `carve.symbols` in `_config.yml`, or `nil` when the key is absent. Resolved once per build. |
|
|
238
|
+
| `reset_symbols` | Drops the cached map. Called from the `site, after_reset` hook this plugin registers, which is what scopes the cache to a build so `jekyll serve --watch` picks up an edit to a symbol file. |
|
|
112
239
|
|
|
113
240
|
## Development
|
|
114
241
|
|
|
115
|
-
|
|
116
|
-
bundle install
|
|
117
|
-
rspec # run the converter unit tests
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## License
|
|
121
|
-
|
|
122
|
-
MIT, markup-carve.
|
|
242
|
+
Contributor setup, testing, and maintenance notes are in the [development guide](docs/development.md).
|
data/lib/jekyll/carve/version.rb
CHANGED
data/lib/jekyll-carve.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
require "jekyll"
|
|
4
6
|
require "carve"
|
|
5
7
|
|
|
@@ -57,7 +59,52 @@ module Jekyll
|
|
|
57
59
|
#
|
|
58
60
|
# Returns the rendered HTML String.
|
|
59
61
|
def convert(content)
|
|
60
|
-
|
|
62
|
+
document = Rendering.current
|
|
63
|
+
root = include_root
|
|
64
|
+
if root.nil? || document.nil? || document.path.to_s.empty?
|
|
65
|
+
return ::Carve.to_html(content.to_s,
|
|
66
|
+
extensions: carve_extensions,
|
|
67
|
+
symbols: carve_symbols)
|
|
68
|
+
end
|
|
69
|
+
# carve-lang added this entry point in 0.1.4; the floor stays lower for sites without includes.
|
|
70
|
+
unless ::Carve.respond_to?(:to_html_with_includes)
|
|
71
|
+
raise ArgumentError,
|
|
72
|
+
"carve.includes: the installed carve-lang (#{::Carve::VERSION}) has no " \
|
|
73
|
+
"to_html_with_includes; upgrade it to 0.1.4 or later, or set carve.includes: false"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
begin
|
|
77
|
+
result = ::Carve.to_html_with_includes(content.to_s,
|
|
78
|
+
root: root,
|
|
79
|
+
source_path: document.path,
|
|
80
|
+
extensions: carve_extensions,
|
|
81
|
+
symbols: carve_symbols)
|
|
82
|
+
rescue ArgumentError => e
|
|
83
|
+
# A relative root, or a page outside it. Both are decided by the
|
|
84
|
+
# engine; what this adds is the key and the page to look at.
|
|
85
|
+
raise ArgumentError,
|
|
86
|
+
"carve.include_root: #{document.relative_path || document.path}: #{e.message}"
|
|
87
|
+
end
|
|
88
|
+
report_includes(result, document, root)
|
|
89
|
+
result[:value]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The containment root configured under `carve.include_root`, or nil when
|
|
93
|
+
# `carve.includes` is off.
|
|
94
|
+
#
|
|
95
|
+
# A configured value is handed to the engine exactly as written. The
|
|
96
|
+
# engine refuses a relative root, and that refusal is what keeps
|
|
97
|
+
# containment off whatever directory the build ran from; expanding it
|
|
98
|
+
# here would mean it never fires. The default is Jekyll's own source
|
|
99
|
+
# directory, which this package derives rather than reads, so that one is
|
|
100
|
+
# expanded.
|
|
101
|
+
def include_root
|
|
102
|
+
return nil unless carve_config["includes"]
|
|
103
|
+
|
|
104
|
+
configured = carve_config["include_root"]
|
|
105
|
+
return configured.to_s if configured.is_a?(String) && !configured.empty?
|
|
106
|
+
|
|
107
|
+
File.expand_path(site_source)
|
|
61
108
|
end
|
|
62
109
|
|
|
63
110
|
# Carve extensions configured under `carve.extensions` in _config.yml.
|
|
@@ -65,11 +112,273 @@ module Jekyll
|
|
|
65
112
|
# Returns an Array of extension names (Strings/Symbols passed through to
|
|
66
113
|
# the engine). Empty when nothing is configured.
|
|
67
114
|
def carve_extensions
|
|
68
|
-
carve_config = @config.is_a?(Hash) ? @config["carve"] : nil
|
|
69
|
-
return [] unless carve_config.is_a?(Hash)
|
|
70
|
-
|
|
71
115
|
Array(carve_config["extensions"])
|
|
72
116
|
end
|
|
117
|
+
|
|
118
|
+
# The `:name:` symbol map configured under `carve.symbols` in _config.yml.
|
|
119
|
+
#
|
|
120
|
+
# Carve parses `:name:` in core, but what a name renders as is a render
|
|
121
|
+
# option, so a document reaching the engine without a map renders the
|
|
122
|
+
# shortcode as its own source text.
|
|
123
|
+
#
|
|
124
|
+
# The key accepts three shapes:
|
|
125
|
+
#
|
|
126
|
+
# carve:
|
|
127
|
+
# symbols: # a mapping, written inline
|
|
128
|
+
# smile: "😄"
|
|
129
|
+
#
|
|
130
|
+
# carve:
|
|
131
|
+
# symbols: _data/symbols.json # a path to a JSON object
|
|
132
|
+
#
|
|
133
|
+
# carve:
|
|
134
|
+
# symbols: # both, merged left to right
|
|
135
|
+
# - _data/emoji.json
|
|
136
|
+
# - { ship: "🚀" }
|
|
137
|
+
#
|
|
138
|
+
# A path is resolved against the site source and clamped inside it, so it
|
|
139
|
+
# names a file in the project and never one outside it.
|
|
140
|
+
#
|
|
141
|
+
# SECURITY: the engine substitutes a symbol value as TRUSTED RAW output -
|
|
142
|
+
# it is NOT escaped, so `{ "l" => "<img src='/l.svg'>" }` emits a real
|
|
143
|
+
# element. carve-rb states the rule this inherits: "NEVER build a symbols
|
|
144
|
+
# map out of untrusted / user-supplied input." That is why the only
|
|
145
|
+
# inputs here are _config.yml and files at paths named in it. Page
|
|
146
|
+
# content and front matter cannot reach this method - `convert` ignores
|
|
147
|
+
# its argument when building the map, and Jekyll hands a converter no
|
|
148
|
+
# front matter at all.
|
|
149
|
+
#
|
|
150
|
+
# Returns a Hash of String name => String value, or nil when nothing is
|
|
151
|
+
# configured (nil, so the engine keeps its own default rather than being
|
|
152
|
+
# told there are no symbols).
|
|
153
|
+
def carve_symbols
|
|
154
|
+
return @symbols if defined?(@symbols)
|
|
155
|
+
|
|
156
|
+
@symbols = build_symbols
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Drop the cached map, so the next render resolves it again.
|
|
160
|
+
#
|
|
161
|
+
# Called from the `:site, :after_reset` hook at the bottom of this file,
|
|
162
|
+
# which is what makes the map resolve once per BUILD rather than once per
|
|
163
|
+
# process. Jekyll instantiates converters from `Site#initialize` while
|
|
164
|
+
# `Site#process` calls only `reset`, so under `jekyll serve --watch` ONE
|
|
165
|
+
# converter instance serves every rebuild - and a map memoized outright
|
|
166
|
+
# would keep serving a file the author has since edited.
|
|
167
|
+
#
|
|
168
|
+
# Returns nothing.
|
|
169
|
+
def reset_symbols
|
|
170
|
+
remove_instance_variable(:@symbols) if defined?(@symbols)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
# Surface what expansion degraded, and record what it read.
|
|
176
|
+
def report_includes(result, document, root)
|
|
177
|
+
page = document.relative_path || document.path
|
|
178
|
+
Array(result[:warnings]).each do |warning|
|
|
179
|
+
origin = warning[:file]
|
|
180
|
+
where = origin && origin != page ? "#{page} (#{origin})" : page
|
|
181
|
+
Jekyll.logger.warn "Carve:", "#{where}: #{warning[:rule]}: #{warning[:message]}"
|
|
182
|
+
end
|
|
183
|
+
suppressed = result[:suppressedWarnings].to_i
|
|
184
|
+
if suppressed.positive?
|
|
185
|
+
Jekyll.logger.warn "Carve:",
|
|
186
|
+
"#{page}: #{suppressed} further include warnings suppressed"
|
|
187
|
+
end
|
|
188
|
+
Array(result[:dependencies]).each do |dependency|
|
|
189
|
+
# A containment refusal and a missing file are the same warning, so a
|
|
190
|
+
# page cannot probe the filesystem (spec I7). The class that was
|
|
191
|
+
# collapsed is here, for the build's own log.
|
|
192
|
+
if dependency[:denial]
|
|
193
|
+
Jekyll.logger.debug "Carve:",
|
|
194
|
+
"#{page}: include #{dependency[:path]}: #{dependency[:denial]}"
|
|
195
|
+
next
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
register_dependency(document, File.join(root, dependency[:path].to_s))
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Tell Jekyll the page has to be rebuilt when this target changes.
|
|
203
|
+
#
|
|
204
|
+
# Only a RESOLVED target is registered. An unresolved dependency reports
|
|
205
|
+
# the directive as written rather than a path relative to the root, so
|
|
206
|
+
# joining it onto the root names the wrong file: a nested `missing.crv`
|
|
207
|
+
# would be recorded against the root rather than against the directory
|
|
208
|
+
# its parent sits in. A fragment appearing later is covered by Jekyll's
|
|
209
|
+
# own watch over the source directory.
|
|
210
|
+
def register_dependency(document, target)
|
|
211
|
+
regenerator = document.site&.regenerator
|
|
212
|
+
return unless regenerator.respond_to?(:add_dependency)
|
|
213
|
+
|
|
214
|
+
regenerator.add_dependency(document.path, target)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# The `carve` table from _config.yml, or an empty Hash.
|
|
218
|
+
def carve_config
|
|
219
|
+
config = @config.is_a?(Hash) ? @config["carve"] : nil
|
|
220
|
+
config.is_a?(Hash) ? config : {}
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# The configured symbol sources, always as an Array.
|
|
224
|
+
#
|
|
225
|
+
# NOT `Array(value)`: a Hash passed to Kernel#Array comes back as an
|
|
226
|
+
# array of PAIRS ({"a" => "b"} becomes [["a", "b"]]), which would silently
|
|
227
|
+
# shred an inline mapping into something that is no longer a map.
|
|
228
|
+
def symbol_sources
|
|
229
|
+
value = carve_config["symbols"]
|
|
230
|
+
case value
|
|
231
|
+
when nil then []
|
|
232
|
+
when Array then value
|
|
233
|
+
else [value]
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# The site source directory a symbol path is resolved against.
|
|
238
|
+
def site_source
|
|
239
|
+
source = @config.is_a?(Hash) ? @config["source"].to_s : ""
|
|
240
|
+
source.empty? ? Dir.pwd : source
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# One configured path, resolved and confined to the site source.
|
|
244
|
+
#
|
|
245
|
+
# Two layers, because they stop different things.
|
|
246
|
+
#
|
|
247
|
+
# `Jekyll.sanitized_path` clamps the LEXICAL path: "../../etc/passwd"
|
|
248
|
+
# comes back as "<source>/etc/passwd", so a written path cannot name a
|
|
249
|
+
# file outside the project.
|
|
250
|
+
#
|
|
251
|
+
# A symlink is not lexical. A link INSIDE the source pointing at a file
|
|
252
|
+
# outside it survives that clamp, and `File.read` follows it - which on a
|
|
253
|
+
# host building a site it did not write would read and potentially
|
|
254
|
+
# publish that file, through a converter that declares `safe true`. So
|
|
255
|
+
# the resolved path is compared against the site source with both sides
|
|
256
|
+
# fully resolved. Jekyll takes the same posture in safe mode: its reader
|
|
257
|
+
# excludes symlinked entries outright.
|
|
258
|
+
def symbol_path(relative_path)
|
|
259
|
+
path = Jekyll.sanitized_path(site_source, relative_path)
|
|
260
|
+
real, root = begin
|
|
261
|
+
[File.realpath(path), File.realpath(site_source)]
|
|
262
|
+
rescue SystemCallError
|
|
263
|
+
# Nothing there to resolve. load_symbol_file raises naming the path.
|
|
264
|
+
return path
|
|
265
|
+
end
|
|
266
|
+
return real if real == root || real.start_with?(root + File::SEPARATOR)
|
|
267
|
+
|
|
268
|
+
raise ArgumentError,
|
|
269
|
+
"carve.symbols: #{relative_path} resolves to #{real}, which is " \
|
|
270
|
+
"outside the site source #{root}"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Merge every configured source, left to right, into one map.
|
|
274
|
+
def build_symbols
|
|
275
|
+
merged = symbol_sources.each_with_object({}) do |source, out|
|
|
276
|
+
case source
|
|
277
|
+
when Hash then out.merge!(normalize_symbols(source, "carve.symbols"))
|
|
278
|
+
when String then out.merge!(load_symbol_file(source))
|
|
279
|
+
else
|
|
280
|
+
raise ArgumentError,
|
|
281
|
+
"carve.symbols: expected a mapping or a path to a JSON file, " \
|
|
282
|
+
"got #{source.class}"
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
merged.empty? ? nil : merged
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Read one JSON file named by carve.symbols.
|
|
289
|
+
#
|
|
290
|
+
# A misconfigured map is raised rather than warned past: every page loses
|
|
291
|
+
# every symbol, and a build that renders `:smile:` as text with a line in
|
|
292
|
+
# the log is the version of this that costs an afternoon.
|
|
293
|
+
def load_symbol_file(relative_path)
|
|
294
|
+
path = symbol_path(relative_path)
|
|
295
|
+
raise ArgumentError, "carve.symbols: no such file: #{path}" unless File.file?(path)
|
|
296
|
+
|
|
297
|
+
begin
|
|
298
|
+
parsed = JSON.parse(File.read(path))
|
|
299
|
+
rescue JSON::ParserError => e
|
|
300
|
+
raise ArgumentError, "carve.symbols: #{path} is not valid JSON: #{e.message}"
|
|
301
|
+
end
|
|
302
|
+
unless parsed.is_a?(Hash)
|
|
303
|
+
raise ArgumentError,
|
|
304
|
+
"carve.symbols: #{path} must hold a JSON object mapping a name " \
|
|
305
|
+
"to its value, got #{parsed.class}"
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
normalize_symbols(parsed, path)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Check one source and key it by String name.
|
|
312
|
+
#
|
|
313
|
+
# These are Hash keys of a shortcode map, NOT Ruby Symbols - `carve.symbols`
|
|
314
|
+
# and the `Strings/Symbols passed through` of `carve_extensions` are
|
|
315
|
+
# different things that share a word. A name IS coerced, because YAML
|
|
316
|
+
# hands back a Symbol or a boolean for some unquoted keys (`:on:` is a
|
|
317
|
+
# perfectly ordinary shortcode to want) and a name never reaches output.
|
|
318
|
+
#
|
|
319
|
+
# A VALUE is not coerced. It goes into the page RAW, so `count: 1` or a
|
|
320
|
+
# nested mapping is a mistake worth stopping the build for rather than
|
|
321
|
+
# something to stringify and emit. The engine draws the same line: it
|
|
322
|
+
# raises TypeError on a non-String value.
|
|
323
|
+
def normalize_symbols(map, origin)
|
|
324
|
+
map.each_with_object({}) do |(name, value), out|
|
|
325
|
+
unless value.is_a?(String)
|
|
326
|
+
raise ArgumentError,
|
|
327
|
+
"#{origin}: the value for #{name.to_s.inspect} must be a string, " \
|
|
328
|
+
"got #{value.class}"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
out[name.to_s] = value
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Which document Jekyll is rendering right now.
|
|
337
|
+
#
|
|
338
|
+
# `Jekyll::Converter#convert` is handed a String and nothing else, so a
|
|
339
|
+
# converter cannot see the file the body came from - and an include has to
|
|
340
|
+
# resolve against that file. The `:pre_render` hook carries it, and Jekyll
|
|
341
|
+
# renders documents one at a time, so the current one is a thread-local
|
|
342
|
+
# rather than a queue.
|
|
343
|
+
module Rendering
|
|
344
|
+
KEY = :jekyll_carve_document
|
|
345
|
+
|
|
346
|
+
module_function
|
|
347
|
+
|
|
348
|
+
def current
|
|
349
|
+
Thread.current[KEY]
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def with(document)
|
|
353
|
+
Thread.current[KEY] = document
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def clear
|
|
357
|
+
Thread.current[KEY] = nil
|
|
358
|
+
end
|
|
73
359
|
end
|
|
74
360
|
end
|
|
75
361
|
end
|
|
362
|
+
|
|
363
|
+
Jekyll::Hooks.register %i[pages documents], :pre_render do |document|
|
|
364
|
+
Jekyll::Carve::Rendering.with(document)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
Jekyll::Hooks.register %i[pages documents], :post_render do |_document|
|
|
368
|
+
Jekyll::Carve::Rendering.clear
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Resolve the symbol map once per build rather than once per process.
|
|
372
|
+
#
|
|
373
|
+
# `Site#reset` runs at the top of every `Site#process`, including every rebuild
|
|
374
|
+
# under `jekyll serve --watch`, and it is the only site event that fires on a
|
|
375
|
+
# rebuild without also re-instantiating the converters. See
|
|
376
|
+
# Jekyll::Carve::Converter#reset_symbols.
|
|
377
|
+
#
|
|
378
|
+
# `converters` is nil the first time this fires: `Site#initialize` calls `reset`
|
|
379
|
+
# BEFORE `setup`, and `setup` is what builds the converter list.
|
|
380
|
+
Jekyll::Hooks.register :site, :after_reset do |site|
|
|
381
|
+
Array(site.converters).each do |converter|
|
|
382
|
+
converter.reset_symbols if converter.is_a?(Jekyll::Carve::Converter)
|
|
383
|
+
end
|
|
384
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-carve
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- markup-carve
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: carve-lang
|
|
@@ -17,6 +17,9 @@ dependencies:
|
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: 0.1.1
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 0.2.0
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -24,6 +27,9 @@ dependencies:
|
|
|
24
27
|
- - ">="
|
|
25
28
|
- !ruby/object:Gem::Version
|
|
26
29
|
version: 0.1.1
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.2.0
|
|
27
33
|
- !ruby/object:Gem::Dependency
|
|
28
34
|
name: jekyll
|
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -68,7 +74,8 @@ files:
|
|
|
68
74
|
homepage: https://github.com/markup-carve/jekyll-carve
|
|
69
75
|
licenses:
|
|
70
76
|
- MIT
|
|
71
|
-
metadata:
|
|
77
|
+
metadata:
|
|
78
|
+
rubygems_mfa_required: 'true'
|
|
72
79
|
post_install_message:
|
|
73
80
|
rdoc_options: []
|
|
74
81
|
require_paths:
|