jekyll-carve 0.1.3 → 0.1.5

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: 28da599466525fea6bbc65a8c6143d8579241a20f1838d6ccb0f9d0317ac3405
4
- data.tar.gz: f36f40e656e8d8ea3da71e207cbaeb61f7daf43b06f8ae7ef41f6d34dd9ad4f7
3
+ metadata.gz: ee0dd725f2e978b4b58399574f0048b8c3b57e065c4b829313a49b19405181b6
4
+ data.tar.gz: 9aafac417e690c501f476bba589f0eb59bdda2987ce9a76b5fd6144ef80c199b
5
5
  SHA512:
6
- metadata.gz: 869155a13081b84dcbd811b2a6474a8e1df7b282924c3b187226c0b16e782d8180f46dfd3b9fa9348c9ed631c0d8b37f33381c7e41849ec6571052c2619cd85b
7
- data.tar.gz: 7fceaae2aee055d86faccc18445e2e5191dba2d9c3a74a73bd91ba1bffac31b531aa7fbcfce58da156379cb42f1f80214b9576a712d488f060e07cde018cd57b
6
+ metadata.gz: 63a663cb509fc3fa5ff68b776be24add79aedf2dfdf3523c2fbab7d0d4d4f3d390015aa027722e8a22e48cda0dec6c3106fdf038179febdc5c245678ec9fc89e
7
+ data.tar.gz: 72d041918581593eef7bb4cd4dd3f57ab96f1d868b9f15cdb207259557c704c150813fbb79df03b30350ff5b3f634ee12f2c7d6aef20254f55d719a12a547a4d
data/README.md CHANGED
@@ -1,242 +1,25 @@
1
1
  # jekyll-carve
2
2
 
3
- A [Jekyll](https://jekyllrb.com) converter plugin for the
4
- [Carve](https://github.com/markup-carve/carve) markup language. It renders
5
- `.crv` pages to HTML by delegating to the native
6
- [`carve-lang`](https://github.com/markup-carve/carve-rb) gem (`Carve.to_html`). No
7
- parser is reimplemented here; this is a thin `Jekyll::Converter` adapter over
8
- the engine.
3
+ Jekyll converter for `.crv` pages, using the carve-lang gem 0.1.11 or later. It renders Carve pages through the native engine. Read the [Carve documentation](https://markup-carve.github.io/carve/) for the language syntax.
9
4
 
10
5
  ## Install
11
6
 
12
- Add both gems to your site's `Gemfile`:
13
-
14
- ```ruby
15
- # Gemfile
16
- gem "jekyll-carve"
17
- gem "carve-lang" # the native Carve engine (a runtime dependency, listed for clarity)
18
- ```
19
-
20
7
  ```sh
21
- bundle install
22
- ```
23
-
24
- Then enable the plugin in `_config.yml`:
25
-
26
- ```yaml
27
- plugins:
28
- - jekyll-carve
29
- ```
30
-
31
- > [!NOTE]
32
- > The `carve-lang` gem ships a Rust native extension and is compiled at install
33
- > time. It requires a Rust toolchain (`cargo`) and Ruby development headers.
34
- > See the carve-rb README for build notes (including the libclang/`stdarg.h`
35
- > workaround on some systems).
36
-
37
- ## Configuration
38
-
39
- Carve engine options are read from `_config.yml` under the `carve` key:
40
-
41
- ```yaml
42
- carve:
43
- extensions:
44
- - heading_permalinks
45
- - math_block
46
- - autolink
47
- ```
48
-
49
- `carve.extensions` is an array of opt-in Carve extension names (Strings or
50
- hyphenated/underscored forms, passed straight through to the engine). When the
51
- key is absent, no extensions are enabled. The recognized extensions are listed
52
- in `Carve::EXTENSIONS`; an unknown name raises `ArgumentError` at build time.
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: "🚀"
8
+ bundle add jekyll-carve
100
9
  ```
101
10
 
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
-
179
11
  ## Usage
180
12
 
181
- Create a page with a `.crv` extension. It MUST begin with Jekyll
182
- YAML front matter, and the Carve body follows below:
183
-
184
- ```text
185
- ---
186
- layout: default
187
- title: Home
188
- ---
189
- # Welcome to *Carve*
190
-
191
- This is /italic/ and *bold* text.
192
-
193
- - Apple
194
- - Banana
195
- ```
196
-
197
- Note Carve's inline syntax: `*...*` is **strong** (bold) and `/.../` is
198
- _emphasis_ (italic) - the opposite of Markdown.
199
-
200
- ## Important: Jekyll front matter vs. Carve frontmatter
201
-
202
- This is the one nuance to understand:
203
-
204
- - **Jekyll only runs a file through a converter if it has YAML front matter**
205
- (a `---` ... `---` block at the very top). A `.crv` file with no front
206
- matter is treated as a static file and copied verbatim, NOT converted.
207
- - **Jekyll strips that front matter before calling the converter.** So the
208
- converter (and therefore the Carve engine) receives only the document
209
- **body**, never the Jekyll front matter.
210
- - **Carve itself also uses `---` for its own frontmatter.** In a Jekyll site
211
- the Jekyll front matter wins: the top `---` ... `---` block is consumed by
212
- Jekyll, and whatever remains is handed to Carve as the body.
213
-
214
- Practical rule: a `.crv` page in Jekyll needs at least an empty front matter
215
- block so Jekyll processes it:
216
-
217
- ```text
218
- ---
219
- ---
220
- # Your Carve content here
13
+ ```yaml
14
+ plugins: [jekyll-carve]
221
15
  ```
222
16
 
223
- Put page metadata (`title`, `layout`, etc.) in the Jekyll front matter. Do not
224
- add a second Carve `---` frontmatter block expecting Carve to parse it; Jekyll
225
- has already removed the leading block by the time Carve runs.
226
-
227
- ## Converter API
228
-
229
- `Jekyll::Carve::Converter < Jekyll::Converter`:
17
+ ## Documentation
230
18
 
231
- | Method | Behavior |
232
- | ------ | -------- |
233
- | `matches(ext)` | `true` for `.crv` (with or without the leading dot, case-insensitive), `false` otherwise. |
234
- | `output_ext(ext)` | `".html"` (includes the dot so Jekyll emits `page.html`). |
235
- | `convert(content)` | `Carve.to_html(content, extensions: configured, symbols: configured)` - the rendered HTML. |
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. |
19
+ - [Carve documentation](https://markup-carve.github.io/carve/)
20
+ - [Documentation index](docs/README.md)
21
+ - [Development](docs/development.md)
239
22
 
240
- ## Development
23
+ ## License
241
24
 
242
- Contributor setup, testing, and maintenance notes are in the [development guide](docs/development.md).
25
+ [MIT](LICENSE)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module Carve
5
- VERSION = "0.1.3"
5
+ VERSION = "0.1.5"
6
6
  end
7
7
  end
data/lib/jekyll-carve.rb CHANGED
@@ -61,32 +61,46 @@ module Jekyll
61
61
  def convert(content)
62
62
  document = Rendering.current
63
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
64
+ include_page = !(root.nil? || document.nil? || document.path.to_s.empty?)
65
+ html = render(content.to_s, document, include_page ? root : nil)
66
+ lint(content.to_s, document, include_page ? root : nil) if lint_mode
67
+ html
68
+ end
75
69
 
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.
70
+ # How `carve.lint` is set: nil (off), :warn or :strict.
71
+ #
72
+ # `true` logs each finding as a build warning; `"strict"` also fails the
73
+ # build once every page has been linted.
74
+ def lint_mode
75
+ case (value = carve_config["lint"])
76
+ when nil, false then nil
77
+ when true then :warn
78
+ when "strict", :strict then :strict
79
+ else
85
80
  raise ArgumentError,
86
- "carve.include_root: #{document.relative_path || document.path}: #{e.message}"
81
+ "carve.lint: expected true, false or \"strict\", got #{value.inspect}"
87
82
  end
88
- report_includes(result, document, root)
89
- result[:value]
83
+ end
84
+
85
+ # Lint findings counted under `carve.lint: strict` since the last reset.
86
+ def lint_findings
87
+ @lint_findings ||= 0
88
+ end
89
+
90
+ # Fail the build when strict lint counted a finding.
91
+ #
92
+ # Called from the `:site, :post_render` hook below, so every page is
93
+ # linted and reported before the build stops.
94
+ def check_lint!
95
+ return unless lint_mode == :strict && lint_findings.positive?
96
+
97
+ raise Jekyll::Errors::FatalException,
98
+ "carve.lint: #{lint_findings} lint finding(s) under strict mode; see the warnings above"
99
+ end
100
+
101
+ # Drop the strict-mode count, so each build is judged on its own.
102
+ def reset_lint
103
+ @lint_findings = 0
90
104
  end
91
105
 
92
106
  # The containment root configured under `carve.include_root`, or nil when
@@ -172,6 +186,71 @@ module Jekyll
172
186
 
173
187
  private
174
188
 
189
+ def render(content, document, root)
190
+ if root.nil?
191
+ return ::Carve.to_html(content,
192
+ extensions: carve_extensions,
193
+ symbols: carve_symbols)
194
+ end
195
+ # carve-lang added this entry point in 0.1.4; the floor stays lower for sites without includes.
196
+ unless ::Carve.respond_to?(:to_html_with_includes)
197
+ raise ArgumentError,
198
+ "carve.includes: the installed carve-lang (#{::Carve::VERSION}) has no " \
199
+ "to_html_with_includes; upgrade it to 0.1.4 or later, or set carve.includes: false"
200
+ end
201
+
202
+ begin
203
+ result = ::Carve.to_html_with_includes(content,
204
+ root: root,
205
+ source_path: document.path,
206
+ extensions: carve_extensions,
207
+ symbols: carve_symbols)
208
+ rescue ArgumentError => e
209
+ # A relative root, or a page outside it. Both are decided by the
210
+ # engine; what this adds is the key and the page to look at.
211
+ raise ArgumentError,
212
+ "carve.include_root: #{document.relative_path || document.path}: #{e.message}"
213
+ end
214
+ report_includes(result, document, root)
215
+ result[:value]
216
+ end
217
+
218
+ # Log each lint finding as a build warning, located in the source file.
219
+ #
220
+ # Runs after rendering, so a bad include root is reported by the render
221
+ # path with its own message. With includes on, the include-* findings
222
+ # repeat the include warnings `report_includes` already logged, so they
223
+ # are counted but not logged twice.
224
+ def lint(content, document, root)
225
+ page = document && (document.relative_path || document.path).to_s
226
+ page = "(no page)" if page.nil? || page.empty?
227
+ options = { extensions: carve_extensions }
228
+ options.update(root: root, source_path: document.path) if root
229
+ findings = ::Carve.lint(content, **options)
230
+ offset = front_matter_lines(document)
231
+ findings.each do |finding|
232
+ next if root && finding[:rule].start_with?("include-")
233
+
234
+ where = if finding[:file]
235
+ "#{page} (#{finding[:file]}:#{finding[:line]}:#{finding[:column]})"
236
+ else
237
+ "#{page}:#{finding[:line] + offset}:#{finding[:column]}"
238
+ end
239
+ Jekyll.logger.warn "Carve lint:", "#{where}: #{finding[:rule]}: #{finding[:message]}"
240
+ end
241
+ @lint_findings = lint_findings + findings.size if lint_mode == :strict
242
+ end
243
+
244
+ # Lines of YAML front matter above the body, so a finding names the line
245
+ # in the file. Jekyll strips the front matter before `convert` sees it.
246
+ def front_matter_lines(document)
247
+ path = document&.path.to_s
248
+ return 0 if path.empty? || !File.file?(path)
249
+
250
+ match = Jekyll::Document::YAML_FRONT_MATTER_REGEXP.match(File.read(path, encoding: "bom|utf-8"))
251
+ match ? match[0].count("\n") : 0
252
+ end
253
+
175
254
  # Surface what expansion degraded, and record what it read.
176
255
  def report_includes(result, document, root)
177
256
  page = document.relative_path || document.path
@@ -379,6 +458,17 @@ end
379
458
  # BEFORE `setup`, and `setup` is what builds the converter list.
380
459
  Jekyll::Hooks.register :site, :after_reset do |site|
381
460
  Array(site.converters).each do |converter|
382
- converter.reset_symbols if converter.is_a?(Jekyll::Carve::Converter)
461
+ next unless converter.is_a?(Jekyll::Carve::Converter)
462
+
463
+ converter.reset_symbols
464
+ converter.reset_lint
465
+ end
466
+ end
467
+
468
+ # `carve.lint: strict` fails here, after every page was linted and before any
469
+ # output is written.
470
+ Jekyll::Hooks.register :site, :post_render do |site|
471
+ Array(site.converters).each do |converter|
472
+ converter.check_lint! if converter.is_a?(Jekyll::Carve::Converter)
383
473
  end
384
474
  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.3
4
+ version: 0.1.5
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-10-08 00:00:00.000000000 Z
11
+ date: 2026-11-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: carve-lang
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.1.1
19
+ version: 0.1.11
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: 0.2.0
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 0.1.1
29
+ version: 0.1.11
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 0.2.0