abqari 1.3.0 → 1.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -1
- data/README.md +15 -8
- data/lib/abqari/audit/csp.rb +17 -22
- data/lib/abqari/helpers.rb +4 -3
- data/lib/abqari/html.rb +29 -0
- data/lib/abqari/image_pipeline.rb +3 -1
- data/lib/abqari/importers/base.rb +13 -8
- data/lib/abqari/minifier.rb +2 -1
- data/lib/abqari/renderer.rb +1 -1
- data/lib/abqari/version.rb +1 -1
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e61ca715dce737000acb0b0f08033cb7dc92d469c0553481d3bbaf6aec1276a
|
|
4
|
+
data.tar.gz: 3e5bf6cae00b055c9a6bd66fed47217b3a2a32f7f5ddc299e1e92200b23b82ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 81e34727adb75ca47e012b73579158fd2a203bda6fe517a1d138b4cee563c4bb54d75926ed6597aa0df826861abe68cc7a36321c24701e3469b2cc4ec6854619
|
|
7
|
+
data.tar.gz: b9f6b8ecf943c8fa60048cf2e0bbe7a64b0b8e27592125d7b62bf47bb0eeee4d318b34583129220dd002b72e7bb6427808e79ec61d08b3316095a9553c7f0dbf
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
_Nothing yet._
|
|
11
11
|
|
|
12
|
+
## [1.3.1] — 2026-10-20
|
|
13
|
+
|
|
14
|
+
Upgrade to correct CSP-audit parsing, HTML text extraction and importer
|
|
15
|
+
escaping. This patch also removes the regex patterns flagged by CodeQL.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- HTML5 parsing uses `nokolexbor`, one runtime dependency with no
|
|
20
|
+
transitive runtime gems. The clean-container CI matrix validates its
|
|
21
|
+
precompiled packages on Ruby 3.3, 3.4 and 4.0.
|
|
22
|
+
- Workshop rendering tests keep an upcoming fixture in memory instead
|
|
23
|
+
of expiring when the example workshop dates pass.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **CSP audits could miss browser-valid inline scripts and handlers.**
|
|
28
|
+
Regex parsing skipped whitespace in closing tags and unquoted
|
|
29
|
+
attributes, and could change script contents while removing comments.
|
|
30
|
+
HTML5 parsing now preserves browser hash input, decodes attributes
|
|
31
|
+
once and ignores inert template contents. Audit tests use the same
|
|
32
|
+
tag rules, including uppercase and comment edge cases.
|
|
33
|
+
- **Excerpts and reading statistics now read HTML as HTML.** Quoted
|
|
34
|
+
delimiters, entities, comments and non-visible script/style/template
|
|
35
|
+
content are handled by a parser rather than a stripping regex.
|
|
36
|
+
Extracted text still requires normal template escaping.
|
|
37
|
+
- **Imported titles retain quotes, backslashes and Unicode YAML line
|
|
38
|
+
breaks without changing the frontmatter structure.** Escaping is a
|
|
39
|
+
single literal-replacement pass, with round-trip regression coverage.
|
|
40
|
+
- **HTML rewrites and CSS minification avoid overlapping whitespace
|
|
41
|
+
matches.** Image and bundle URL rewrites retain their output while
|
|
42
|
+
regex matching no longer needs to reconsider delimiter whitespace.
|
|
43
|
+
|
|
12
44
|
## [1.3.0] — 2026-10-11
|
|
13
45
|
|
|
14
46
|
If CI runs `bin/audit --strict`, run `bin/audit` locally before
|
|
@@ -1141,7 +1173,8 @@ run a real working author / publisher site.
|
|
|
1141
1173
|
commonmarker ≥ 2.4 ships precompiled 4.0 binaries, which is what
|
|
1142
1174
|
the `~> 2.4` floor bought.
|
|
1143
1175
|
|
|
1144
|
-
[Unreleased]: https://github.com/grantrayner/abqari/compare/v1.3.
|
|
1176
|
+
[Unreleased]: https://github.com/grantrayner/abqari/compare/v1.3.1...HEAD
|
|
1177
|
+
[1.3.1]: https://github.com/grantrayner/abqari/compare/v1.3.0...v1.3.1
|
|
1145
1178
|
[1.3.0]: https://github.com/grantrayner/abqari/compare/v1.2.0...v1.3.0
|
|
1146
1179
|
[1.2.0]: https://github.com/grantrayner/abqari/compare/v1.1.2...v1.2.0
|
|
1147
1180
|
[1.1.2]: https://github.com/grantrayner/abqari/compare/v1.1.1...v1.1.2
|
data/README.md
CHANGED
|
@@ -11,7 +11,7 @@ optional overrides), while the engine — layouts, themes, helpers, asset
|
|
|
11
11
|
pipeline — lives in the dependency. Forking the engine is also
|
|
12
12
|
supported for users who want to own all the code.
|
|
13
13
|
|
|
14
|
-
> **Status:** v1.3.
|
|
14
|
+
> **Status:** v1.3.1 — see
|
|
15
15
|
> [CHANGELOG.md](https://github.com/grantrayner/abqari/blob/main/CHANGELOG.md).
|
|
16
16
|
> Distributed via RubyGems (`gem install abqari`) and as a Bundler
|
|
17
17
|
> path/git source. The API follows [semver](https://semver.org/) from
|
|
@@ -38,10 +38,12 @@ Reporting a vulnerability:
|
|
|
38
38
|
|
|
39
39
|
## Design principles
|
|
40
40
|
|
|
41
|
-
1. **Minimal dependencies.**
|
|
42
|
-
|
|
43
|
-
the
|
|
44
|
-
|
|
41
|
+
1. **Minimal dependencies.** Eight runtime gems cover Markdown,
|
|
42
|
+
auto-escaped templates, images, HTML5 parsing, syntax highlighting,
|
|
43
|
+
the dev server, and CSV/Base64 handling. `nokolexbor` reads HTML for
|
|
44
|
+
CSP audits and text extraction, with no transitive runtime dependencies
|
|
45
|
+
and precompiled packages for the supported desktop/Linux platforms.
|
|
46
|
+
Content importing needs one more (`reverse_markdown`),
|
|
45
47
|
deliberately left out of the runtime set so a normal install doesn't
|
|
46
48
|
pull in nokogiri for a feature it never uses.
|
|
47
49
|
2. **Ruby-first.** ERB templates, real Ruby in views.
|
|
@@ -174,7 +176,7 @@ Test that it worked:
|
|
|
174
176
|
abqari version
|
|
175
177
|
```
|
|
176
178
|
|
|
177
|
-
You should see a version number, e.g. `abqari 1.3.
|
|
179
|
+
You should see a version number, e.g. `abqari 1.3.1`. If you get
|
|
178
180
|
`command not found`, see [Troubleshooting](#troubleshooting) below.
|
|
179
181
|
|
|
180
182
|
### Step 3 — Create your first site
|
|
@@ -470,7 +472,7 @@ end
|
|
|
470
472
|
Or — pin to a git ref while tracking pre-release work:
|
|
471
473
|
|
|
472
474
|
```ruby
|
|
473
|
-
gem 'abqari', git: 'https://github.com/grantrayner/abqari.git', tag: 'v1.3.
|
|
475
|
+
gem 'abqari', git: 'https://github.com/grantrayner/abqari.git', tag: 'v1.3.1'
|
|
474
476
|
```
|
|
475
477
|
|
|
476
478
|
Install the libvips C library once (hard dep for the image pipeline):
|
|
@@ -947,6 +949,11 @@ and on `Abqari::RenderContext`, available everywhere ERB runs.
|
|
|
947
949
|
| `reading_time(text, wpm:)` | `reading_time(post.body)` → `"4 min read"` |
|
|
948
950
|
| `slugify(text)` | `slugify('Hello World!')` → `"hello-world"` |
|
|
949
951
|
|
|
952
|
+
`excerpt`, `word_count` and `reading_time` parse HTML and decode entities;
|
|
953
|
+
comments and script/style/template contents do not count as visible text.
|
|
954
|
+
An excerpt is plain text, not sanitized HTML. Render it with `<%= excerpt(...) %>`
|
|
955
|
+
so template auto-escaping remains in force.
|
|
956
|
+
|
|
950
957
|
**Time**
|
|
951
958
|
|
|
952
959
|
| Helper | Example |
|
|
@@ -2566,7 +2573,7 @@ changes to your site. To track a fork or an unreleased branch
|
|
|
2566
2573
|
instead, swap the line for a git source:
|
|
2567
2574
|
|
|
2568
2575
|
```ruby
|
|
2569
|
-
gem 'abqari', git: 'https://github.com/grantrayner/abqari.git', tag: 'v1.3.
|
|
2576
|
+
gem 'abqari', git: 'https://github.com/grantrayner/abqari.git', tag: 'v1.3.1'
|
|
2570
2577
|
```
|
|
2571
2578
|
|
|
2572
2579
|
The cadence: read [CHANGELOG.md](https://github.com/grantrayner/abqari/blob/main/CHANGELOG.md) before each upgrade,
|
data/lib/abqari/audit/csp.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'base64'
|
|
4
|
-
require 'cgi'
|
|
5
4
|
require 'digest'
|
|
5
|
+
require_relative '../html'
|
|
6
6
|
|
|
7
7
|
module Abqari
|
|
8
8
|
class Audit
|
|
@@ -114,35 +114,30 @@ module Abqari
|
|
|
114
114
|
# Each piece of inline code as `[kind, content, script_type]`, where
|
|
115
115
|
# content is exactly what the browser hashes.
|
|
116
116
|
def inline_code(html)
|
|
117
|
-
html = html.gsub(/<!--.*?-->/m, '')
|
|
118
117
|
code = []
|
|
118
|
+
Html.document(html).css('*').each do |node|
|
|
119
|
+
next if node.ancestors.any? { |parent| parent.name == 'template' }
|
|
120
|
+
|
|
121
|
+
if node.name == 'script' && !node.key?('src')
|
|
122
|
+
type = node['type']&.strip&.downcase
|
|
123
|
+
if type.nil? || type.empty? || EXECUTABLE_TYPES.include?(type)
|
|
124
|
+
code << [:script, node.content, type]
|
|
125
|
+
end
|
|
126
|
+
elsif node.name == 'style'
|
|
127
|
+
code << [:style, node.content]
|
|
128
|
+
end
|
|
119
129
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
next unless type.nil? || type.empty? || EXECUTABLE_TYPES.include?(type)
|
|
130
|
+
# The parser already decodes attributes once, as the browser
|
|
131
|
+
# does. Decoding again changes the bytes used for CSP hashes.
|
|
132
|
+
node.attributes.each do |name, attribute|
|
|
133
|
+
next unless name == 'style' || name.match?(/\Aon[a-z]+\z/)
|
|
125
134
|
|
|
126
|
-
|
|
127
|
-
end
|
|
128
|
-
html.scan(%r{<style\b[^>]*>(.*?)</style>}mi).each { |(body)| code << [:style, body] }
|
|
129
|
-
|
|
130
|
-
# Attributes, with script and style bodies removed first so a
|
|
131
|
-
# string inside a script that looks like markup isn't counted.
|
|
132
|
-
tags_only = html.gsub(%r{<(script|style)\b[^>]*>.*?</\1>}mi, '')
|
|
133
|
-
tags_only.scan(/<[a-zA-Z][^>]*>/).each do |tag|
|
|
134
|
-
tag.scan(/\s(on[a-z]+|style)\s*=\s*(?:"([^"]*)"|'([^']*)')/i).each do |name, dq, sq|
|
|
135
|
-
code << [name.casecmp?('style') ? :style_attr : :script_attr, CGI.unescapeHTML(dq || sq)]
|
|
135
|
+
code << [name == 'style' ? :style_attr : :script_attr, attribute.value]
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
138
|
code
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
def attribute(attrs, name)
|
|
142
|
-
match = attrs.match(/\b#{name}\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i)
|
|
143
|
-
match && (match[1] || match[2] || match[3])
|
|
144
|
-
end
|
|
145
|
-
|
|
146
141
|
# Mirrors the CSP spec's inline checks: a hash or nonce in the list
|
|
147
142
|
# switches `'unsafe-inline'` off; attribute hashes count only with
|
|
148
143
|
# `'unsafe-hashes'`. `'inline-speculation-rules'` is honoured only
|
data/lib/abqari/helpers.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'date'
|
|
|
5
5
|
require 'json'
|
|
6
6
|
require 'time'
|
|
7
7
|
require 'uri'
|
|
8
|
+
require_relative 'html'
|
|
8
9
|
|
|
9
10
|
module Abqari
|
|
10
11
|
module Helpers
|
|
@@ -161,7 +162,7 @@ end
|
|
|
161
162
|
# `data-heading-content`, so each heading silently added several
|
|
162
163
|
# phantom "words" to the structured data search engines read.
|
|
163
164
|
def word_count(text)
|
|
164
|
-
text
|
|
165
|
+
Html.text(text, separator: ' ').scan(/\S+/).length
|
|
165
166
|
end
|
|
166
167
|
|
|
167
168
|
# Format a date or time. Default format is "1 January 2026"
|
|
@@ -224,7 +225,7 @@ end
|
|
|
224
225
|
end
|
|
225
226
|
|
|
226
227
|
def excerpt(text, words: 50, omission: '…')
|
|
227
|
-
stripped = text
|
|
228
|
+
stripped = Html.text(text).strip
|
|
228
229
|
arr = stripped.split(/\s+/)
|
|
229
230
|
return stripped if arr.length <= words
|
|
230
231
|
|
|
@@ -232,7 +233,7 @@ end
|
|
|
232
233
|
end
|
|
233
234
|
|
|
234
235
|
def reading_time(text, wpm: 200)
|
|
235
|
-
stripped = text
|
|
236
|
+
stripped = Html.text(text)
|
|
236
237
|
word_count = stripped.split(/\s+/).length
|
|
237
238
|
# Clamp wpm to ≥1 (a `wpm: 0` would divide to Infinity → FloatDomainError)
|
|
238
239
|
# and the result to ≥1 minute (empty text shouldn't read "0 min read").
|
data/lib/abqari/html.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'nokolexbor'
|
|
4
|
+
|
|
5
|
+
module Abqari
|
|
6
|
+
# HTML must be read with the same tag, comment and attribute rules as
|
|
7
|
+
# the browser. Regex stripping can change script hash input or hide an
|
|
8
|
+
# executable tag. Lexbor also keeps this work out of Ruby's regex engine.
|
|
9
|
+
module Html
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def document(html)
|
|
13
|
+
Nokolexbor::HTML(html.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fragment(html)
|
|
17
|
+
Nokolexbor::DocumentFragment.parse(html.to_s)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Plain text, never trusted HTML. Callers must still escape it when
|
|
21
|
+
# rendering (the normal <%= interpolation does this automatically).
|
|
22
|
+
# Non-visible code and comments must not inflate reading estimates.
|
|
23
|
+
def text(html, separator: '')
|
|
24
|
+
tree = fragment(html)
|
|
25
|
+
tree.css('script, style, template').each(&:remove)
|
|
26
|
+
tree.xpath('.//text()').map(&:content).join(separator)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -173,7 +173,9 @@ module Abqari
|
|
|
173
173
|
def post_process_html(html, page: nil)
|
|
174
174
|
return html unless enabled?
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
# Whitespace belongs to the delimiter, not to the attributes.
|
|
177
|
+
# Possessive quantifiers prevent overlapping backtracking paths.
|
|
178
|
+
html.gsub(/<img\s++([^>]*+)>/m) do |match|
|
|
177
179
|
attrs = ::Regexp.last_match(1)
|
|
178
180
|
src = attrs[/src="([^"]+)"/, 1]
|
|
179
181
|
next match unless src
|
|
@@ -285,7 +285,7 @@ module Abqari
|
|
|
285
285
|
# been written straight through, turning one mapping entry into
|
|
286
286
|
# two. Importer input is a third-party export, so this is
|
|
287
287
|
# attacker-shaped. Escape them into their YAML forms instead.
|
|
288
|
-
if str.match?(/[\
|
|
288
|
+
if str.match?(/[\u0000-\u001F\u007F-\u009F\u2028\u2029]/)
|
|
289
289
|
return %("#{escape_yaml_double_quoted(str)}")
|
|
290
290
|
end
|
|
291
291
|
|
|
@@ -301,13 +301,18 @@ module Abqari
|
|
|
301
301
|
# quotes, then the control characters that have named escapes,
|
|
302
302
|
# then anything else remaining as \uXXXX.
|
|
303
303
|
def escape_yaml_double_quoted(str)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
304
|
+
# A block returns literal replacement bytes: String#gsub's
|
|
305
|
+
# replacement-string backslash rules cannot reinterpret them.
|
|
306
|
+
str.gsub(/[\\"\u0000-\u001F\u007F-\u009F\u2028\u2029]/) do |char|
|
|
307
|
+
case char
|
|
308
|
+
when '\\' then '\\\\'
|
|
309
|
+
when '"' then '\\"'
|
|
310
|
+
when "\n" then '\\n'
|
|
311
|
+
when "\r" then '\\r'
|
|
312
|
+
when "\t" then '\\t'
|
|
313
|
+
else format('\\u%04X', char.ord)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
311
316
|
end
|
|
312
317
|
|
|
313
318
|
def block_array(key, items)
|
data/lib/abqari/minifier.rb
CHANGED
data/lib/abqari/renderer.rb
CHANGED
|
@@ -810,7 +810,7 @@ module Abqari
|
|
|
810
810
|
def rewrite_bundle_urls(html, page)
|
|
811
811
|
base = page.url
|
|
812
812
|
|
|
813
|
-
html.gsub(/(<(?:img|a)\s
|
|
813
|
+
html.gsub(/(<(?:img|a)\s++[^>]*?(?:src|href)=")([^"]+)/) do
|
|
814
814
|
prefix = ::Regexp.last_match(1)
|
|
815
815
|
url = ::Regexp.last_match(2)
|
|
816
816
|
|
data/lib/abqari/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: abqari
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Grant Rayner
|
|
@@ -65,6 +65,20 @@ dependencies:
|
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '1.13'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: nokolexbor
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 0.8.0
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 0.8.0
|
|
68
82
|
- !ruby/object:Gem::Dependency
|
|
69
83
|
name: rouge
|
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -239,6 +253,7 @@ files:
|
|
|
239
253
|
- lib/abqari/headers_file.rb
|
|
240
254
|
- lib/abqari/helpers.rb
|
|
241
255
|
- lib/abqari/hooks.rb
|
|
256
|
+
- lib/abqari/html.rb
|
|
242
257
|
- lib/abqari/http_safe.rb
|
|
243
258
|
- lib/abqari/image_pipeline.rb
|
|
244
259
|
- lib/abqari/importers.rb
|