jekyll-fast-reader 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +273 -0
- data/assets/fast-reader.css +42 -0
- data/assets/fast-reader.js +47 -0
- data/lib/jekyll/fast_reader/anchor_calculator.rb +23 -0
- data/lib/jekyll/fast_reader/asset_generator.rb +23 -0
- data/lib/jekyll/fast_reader/asset_static_file.rb +20 -0
- data/lib/jekyll/fast_reader/characters.rb +21 -0
- data/lib/jekyll/fast_reader/configuration.rb +77 -0
- data/lib/jekyll/fast_reader/document_filter.rb +36 -0
- data/lib/jekyll/fast_reader/hooks.rb +33 -0
- data/lib/jekyll/fast_reader/html_walker.rb +53 -0
- data/lib/jekyll/fast_reader/liquid_filters.rb +39 -0
- data/lib/jekyll/fast_reader/stop_words.rb +21 -0
- data/lib/jekyll/fast_reader/text_processor.rb +52 -0
- data/lib/jekyll/fast_reader/tokenizer.rb +19 -0
- data/lib/jekyll/fast_reader/transformer.rb +86 -0
- data/lib/jekyll/fast_reader/version.rb +5 -0
- data/lib/jekyll-fast-reader.rb +21 -0
- metadata +148 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 97561b14aaba228d2fa964d79b12d3036afb8193c187c5a81639559d95e49310
|
|
4
|
+
data.tar.gz: 2fc8049462c810fca413c2b375d0138be52218fb9f0386a567fd2d41f78e6a2b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 222ca73894731ca26dcac052c70708f3cbad96fee091f60e20df808393c2f1cb6c1fd573ba70ce4481c1480c609b089bc57838cebeb3b06fe331b441bf257710
|
|
7
|
+
data.tar.gz: 2ce8a523077f9c6cb1cbf8713632c8f591efd40bf4e684ea6bce3db4156103028f063f2426ff71addb9474f7bc346f0f5ca8cdef65f0370cc4f145af8dffdd17
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 developerlee79
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# jekyll-fast-reader
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/jekyll-fast-reader)
|
|
4
|
+
[](LICENSE.txt)
|
|
5
|
+
[](https://www.ruby-lang.org/)
|
|
6
|
+
|
|
7
|
+
Bionic Reading bolds the opening characters of each word so your eye anchors faster and your brain fills in the rest. This plugin wires that technique directly into your Jekyll build — no JavaScript, no manual markup.
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Getting Started](#getting-started)
|
|
11
|
+
- [Configuration](#configuration)
|
|
12
|
+
- [Liquid Filters](#liquid-filters)
|
|
13
|
+
- [How It Works](#how-it-works)
|
|
14
|
+
- [Development](#development)
|
|
15
|
+
- [Contributing](#contributing)
|
|
16
|
+
- [License](#license)
|
|
17
|
+
|
|
18
|
+
<br>
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
### With Bundler
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# Gemfile
|
|
26
|
+
gem "jekyll-fast-reader", "~> 0.1"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then run:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bundle install
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Manual
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
gem install jekyll-fast-reader
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Add the plugin to your site's `_config.yml`:
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
plugins:
|
|
45
|
+
- jekyll-fast-reader
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requires Jekyll >= 4.0, < 5.0 and Ruby >= 2.7.
|
|
49
|
+
|
|
50
|
+
### System dependencies
|
|
51
|
+
|
|
52
|
+
Nokogiri ships native extensions. On a fresh machine you may also need:
|
|
53
|
+
|
|
54
|
+
- **macOS:** `xcode-select --install`
|
|
55
|
+
- **Debian / Ubuntu:** `sudo apt install build-essential libxml2-dev libxslt1-dev`
|
|
56
|
+
- **Fedora / RHEL:** `sudo dnf install gcc make libxml2-devel libxslt-devel`
|
|
57
|
+
|
|
58
|
+
### Compatibility
|
|
59
|
+
|
|
60
|
+
Works alongside [`jekyll-polyglot`](https://github.com/untra/polyglot) — the stylesheet and toggle script are re-emitted on every language pass so each localized copy of the site gets its own asset under its language prefix.
|
|
61
|
+
|
|
62
|
+
<br>
|
|
63
|
+
|
|
64
|
+
## Getting Started
|
|
65
|
+
|
|
66
|
+
No template changes needed. Install the plugin and build — the stylesheet is injected into `<head>` automatically.
|
|
67
|
+
|
|
68
|
+
**Build and serve**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bundle exec jekyll serve
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Words in configured collections will have their opening characters bolded. The plugin wraps anchors in `<span class="fr-anchor">` — for example:
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
<!-- input -->
|
|
78
|
+
<p>The quick brown fox jumps</p>
|
|
79
|
+
|
|
80
|
+
<!-- output -->
|
|
81
|
+
<p>The <span class="fr-anchor">qu</span>ick <span class="fr-anchor">br</span>own <span class="fr-anchor">fo</span>x <span class="fr-anchor">ju</span>mps</p>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
("The" is a stop word and is left unwrapped.)
|
|
85
|
+
|
|
86
|
+
<br>
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
If `_config.yml` defines `baseurl`, it is automatically prepended to both `css_output_path` and `js_output_path` so the injected `<link>` and `<script>` tags resolve under your site's prefix. Trailing slashes on `baseurl` are normalized.
|
|
91
|
+
|
|
92
|
+
All options are optional. Add a `fast_reader` key to `_config.yml` to override any default:
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
fast_reader:
|
|
96
|
+
enabled: true
|
|
97
|
+
collections:
|
|
98
|
+
- posts
|
|
99
|
+
exclude_selectors:
|
|
100
|
+
- code
|
|
101
|
+
- pre
|
|
102
|
+
- script
|
|
103
|
+
- style
|
|
104
|
+
- kbd
|
|
105
|
+
- samp
|
|
106
|
+
css_output_path: /assets/fast-reader.css
|
|
107
|
+
stop_words_extra: []
|
|
108
|
+
toggle: false
|
|
109
|
+
default_on: true
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| Key | Default | Description |
|
|
113
|
+
|-----|---------|-------------|
|
|
114
|
+
| `enabled` | `true` | Set to `false` to disable the plugin site-wide. To opt out a single document, use `fast_reader: false` in its front matter instead. |
|
|
115
|
+
| `collections` | `["posts"]` | Which Jekyll collections to process. Accepts either an Array of labels or a Hash mapping label → `true`/`false`/options-hash — see [Per-collection options](#per-collection-options). Pages do not belong to a collection — see [Per-document override](#per-document-override) to enable a single page. |
|
|
116
|
+
| `exclude_selectors` | see above | CSS selectors whose inner text is left untouched. **Replaces** the default list entirely — include the defaults if you want to keep them. |
|
|
117
|
+
| `css_output_path` | `"/assets/fast-reader.css"` | URL path for the generated stylesheet `<link>` |
|
|
118
|
+
| `js_output_path` | `"/assets/fast-reader.js"` | URL path for the toggle behavior script (only injected when `toggle: true`) |
|
|
119
|
+
| `stop_words_extra` | `[]` | Additional words to skip (merged with the built-in list) |
|
|
120
|
+
| `toggle` | `false` | Set to `true` to inject a fixed toggle button that enables/disables the effect at runtime without a rebuild. The button is wired by an external `fast-reader.js` (no inline `onclick`/`style`, CSP-safe) and persists state across navigation under `localStorage["fr-state"]` (`"on"` / `"off"`). Keyboard shortcut: <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>B</kbd>. |
|
|
121
|
+
| `default_on` | `true` | When `true`, anchors render bold by default; the toggle removes the effect by adding `fr-disabled` to `<body>`. When `false`, anchors render with inherited weight by default; the plugin marks `<body>` with `fr-opt-in` at build time, and the toggle activates the effect by also adding `fast-reader`. |
|
|
122
|
+
|
|
123
|
+
### Per-document override
|
|
124
|
+
|
|
125
|
+
Front matter flips the decision for a single document. Both opt-in and opt-out are supported:
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
---
|
|
129
|
+
title: My Post
|
|
130
|
+
fast_reader: false # opt this document out, even if its collection is processed
|
|
131
|
+
---
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
---
|
|
136
|
+
title: A Standalone Page
|
|
137
|
+
fast_reader: true # opt a page in, even though pages aren't in `collections`
|
|
138
|
+
---
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Use `fast_reader: true` to enable Fast Reader on top-level pages (`about.md`, `index.html`, etc.) since they do not belong to any Jekyll collection.
|
|
142
|
+
|
|
143
|
+
### Stop words
|
|
144
|
+
|
|
145
|
+
Common words — articles, prepositions, short conjunctions — are skipped by default so they don't get distracting anchors. The built-in list is:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
a, an, the, of, in, on, at, to, by, or, is, as,
|
|
149
|
+
and, but, for, nor, so, yet,
|
|
150
|
+
it, its, this, that
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Matching is case-insensitive. Extend the list per site:
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
fast_reader:
|
|
157
|
+
stop_words_extra:
|
|
158
|
+
- jekyll
|
|
159
|
+
- ruby
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Per-element opt-out
|
|
163
|
+
|
|
164
|
+
Add `data-fr-skip` to any element to prevent its inner text (and all descendants) from being processed. Useful for callouts, brand names, or quoted blocks that should keep their original weight:
|
|
165
|
+
|
|
166
|
+
```html
|
|
167
|
+
<p>Most text gets bionic anchors.</p>
|
|
168
|
+
<aside data-fr-skip>
|
|
169
|
+
<p>This block is left alone.</p>
|
|
170
|
+
</aside>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This is finer-grained than `exclude_selectors`, which is global, and does not require touching site config.
|
|
174
|
+
|
|
175
|
+
### Per-collection options
|
|
176
|
+
|
|
177
|
+
`collections` also accepts a Hash so you can tune per-collection behavior:
|
|
178
|
+
|
|
179
|
+
```yaml
|
|
180
|
+
fast_reader:
|
|
181
|
+
stop_words_extra: [jekyll]
|
|
182
|
+
collections:
|
|
183
|
+
posts: true # default behavior
|
|
184
|
+
drafts: false # explicitly skipped
|
|
185
|
+
notes:
|
|
186
|
+
stop_words_extra: [observability, cache] # adds to the global list, for this collection only
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`true` is equivalent to listing the label in the legacy Array form. `false` skips the collection even if it exists. A Hash value lets you override `stop_words_extra` per collection — values are concatenated ahead of the global list (per-collection words win on duplicates).
|
|
190
|
+
|
|
191
|
+
### Accessibility
|
|
192
|
+
|
|
193
|
+
- **Keyboard shortcut.** When `toggle: true`, the button can be flipped with <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>B</kbd>. The shortcut is suppressed while focus is inside an `<input>`, `<textarea>`, `<select>`, or any `contenteditable` element, so it never blocks typing. The button advertises this via `aria-keyshortcuts="Alt+Shift+B"` for assistive tech.
|
|
194
|
+
- **Focus indicator.** `#fr-toggle:focus-visible` paints a 2 px outline so keyboard users can see when the button is focused.
|
|
195
|
+
- **Reduced motion / visual emphasis.** Users with `prefers-reduced-motion: reduce` set in their OS get a stylesheet rule that resets `.fr-anchor` back to inherited weight — the bionic anchors disappear without requiring them to click the toggle.
|
|
196
|
+
- **Print.** Anchors are also reset when printing (`@media print`), so the printed output matches normal weight.
|
|
197
|
+
|
|
198
|
+
<br>
|
|
199
|
+
|
|
200
|
+
## Liquid Filters
|
|
201
|
+
|
|
202
|
+
The plugin registers three Liquid filters you can use anywhere in your templates:
|
|
203
|
+
|
|
204
|
+
| Filter | Description |
|
|
205
|
+
|--------|-------------|
|
|
206
|
+
| `reading_time` | Returns a string like `"4 min"` based on a 250 wpm reading rate. HTML tags are stripped before counting. |
|
|
207
|
+
| `word_count` | Returns the integer word count of the input, ignoring HTML tags. |
|
|
208
|
+
| `fast_reader` | Wraps anchors around the words of a plain string — useful for excerpts, headings, or any text you want anchored without going through the document-level pipeline. Honors site-level `stop_words_extra`. |
|
|
209
|
+
|
|
210
|
+
```liquid
|
|
211
|
+
<p class="meta">{{ content | reading_time }} · {{ content | word_count }} words</p>
|
|
212
|
+
|
|
213
|
+
<h2>{{ post.title | fast_reader }}</h2>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
<br>
|
|
217
|
+
|
|
218
|
+
## How It Works
|
|
219
|
+
|
|
220
|
+
On each Jekyll build the plugin registers a post-render hook. For every document in the configured collections (and any page or document with `fast_reader: true` in its front matter) it:
|
|
221
|
+
|
|
222
|
+
1. Parses the rendered HTML with Nokogiri
|
|
223
|
+
2. Walks all text nodes, skipping `exclude_selectors`
|
|
224
|
+
3. Tokenizes text into words, whitespace, and punctuation
|
|
225
|
+
4. Skips stop words, tokens containing digits, and non-Latin tokens
|
|
226
|
+
5. Wraps the opening characters of each qualifying word in `<span class="fr-anchor">`
|
|
227
|
+
6. Replaces each text node with the enhanced fragment
|
|
228
|
+
|
|
229
|
+
The shipped stylesheet supports both `default_on` modes:
|
|
230
|
+
|
|
231
|
+
```css
|
|
232
|
+
/* default_on: true (default) — bold by default, toggle disables */
|
|
233
|
+
.fr-anchor { font-weight: bold; }
|
|
234
|
+
.fr-disabled .fr-anchor { font-weight: inherit; }
|
|
235
|
+
|
|
236
|
+
/* default_on: false — inherited weight by default, toggle activates */
|
|
237
|
+
.fr-opt-in .fr-anchor { font-weight: inherit; }
|
|
238
|
+
.fr-opt-in.fast-reader .fr-anchor { font-weight: bold; }
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
In `default_on: false` mode the plugin adds `fr-opt-in` to `<body>` at build time so the second pair of selectors becomes active. The toggle button (when enabled) flips the appropriate body class for the current mode.
|
|
242
|
+
|
|
243
|
+
The toggle button carries `data-fr-mode="default-on"` or `data-fr-mode="opt-in"`; the external `fast-reader.js` reads that attribute to decide whether to flip `fr-disabled` or `fast-reader` on `<body>`, then mirrors the choice into `localStorage["fr-state"]` so it sticks across pages.
|
|
244
|
+
|
|
245
|
+
Anchor length scales with word length:
|
|
246
|
+
|
|
247
|
+
| Word length | Bolded characters |
|
|
248
|
+
|-------------|-------------------|
|
|
249
|
+
| 1–3 | 1 |
|
|
250
|
+
| 4–6 | 2 |
|
|
251
|
+
| 7–9 | 3 |
|
|
252
|
+
| 10+ | 40% (ceiling) |
|
|
253
|
+
|
|
254
|
+
<br>
|
|
255
|
+
|
|
256
|
+
## Development
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
bundle install
|
|
260
|
+
bundle exec rake spec # run the test suite (RSpec, with SimpleCov coverage)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
<br>
|
|
264
|
+
|
|
265
|
+
## Contributing
|
|
266
|
+
|
|
267
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/developerlee79/jekyll-fast-reader). This project is intended to be a safe, welcoming space for collaboration.
|
|
268
|
+
|
|
269
|
+
<br>
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
[MIT](LICENSE.txt)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
.fr-anchor {
|
|
2
|
+
font-weight: bold;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.fr-disabled .fr-anchor {
|
|
6
|
+
font-weight: inherit;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.fr-opt-in .fr-anchor {
|
|
10
|
+
font-weight: inherit;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.fr-opt-in.fast-reader .fr-anchor {
|
|
14
|
+
font-weight: bold;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#fr-toggle {
|
|
18
|
+
position: fixed;
|
|
19
|
+
bottom: 1rem;
|
|
20
|
+
right: 1rem;
|
|
21
|
+
z-index: 9999;
|
|
22
|
+
padding: 0.4rem 0.8rem;
|
|
23
|
+
font-size: 0.8rem;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#fr-toggle:focus-visible {
|
|
28
|
+
outline: 2px solid currentColor;
|
|
29
|
+
outline-offset: 2px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@media print {
|
|
33
|
+
.fr-anchor {
|
|
34
|
+
font-weight: inherit;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@media (prefers-reduced-motion: reduce) {
|
|
39
|
+
.fr-anchor {
|
|
40
|
+
font-weight: inherit;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var STORAGE_KEY = 'fr-state';
|
|
5
|
+
var btn = document.getElementById('fr-toggle');
|
|
6
|
+
if (!btn) return;
|
|
7
|
+
|
|
8
|
+
var mode = btn.getAttribute('data-fr-mode');
|
|
9
|
+
|
|
10
|
+
function applyState(active) {
|
|
11
|
+
if (mode === 'opt-in') {
|
|
12
|
+
document.body.classList.toggle('fast-reader', active);
|
|
13
|
+
} else {
|
|
14
|
+
document.body.classList.toggle('fr-disabled', !active);
|
|
15
|
+
}
|
|
16
|
+
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
var saved = localStorage.getItem(STORAGE_KEY);
|
|
21
|
+
if (saved === 'on') applyState(true);
|
|
22
|
+
else if (saved === 'off') applyState(false);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
// localStorage may be unavailable (private mode, disabled cookies, etc.)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
btn.addEventListener('click', function () {
|
|
28
|
+
var active = btn.getAttribute('aria-pressed') !== 'true';
|
|
29
|
+
applyState(active);
|
|
30
|
+
try {
|
|
31
|
+
localStorage.setItem(STORAGE_KEY, active ? 'on' : 'off');
|
|
32
|
+
} catch (e) {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
document.addEventListener('keydown', function (e) {
|
|
38
|
+
if (!e.altKey || !e.shiftKey) return;
|
|
39
|
+
if (e.key !== 'B' && e.key !== 'b') return;
|
|
40
|
+
|
|
41
|
+
var t = e.target;
|
|
42
|
+
if (t && (t.isContentEditable || /^(input|textarea|select)$/i.test(t.tagName))) return;
|
|
43
|
+
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
btn.click();
|
|
46
|
+
});
|
|
47
|
+
})();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Jekyll
|
|
2
|
+
module FastReader
|
|
3
|
+
class AnchorCalculator
|
|
4
|
+
BUCKET_MAP = {
|
|
5
|
+
1 => 1,
|
|
6
|
+
2 => 1,
|
|
7
|
+
3 => 1,
|
|
8
|
+
4 => 2,
|
|
9
|
+
5 => 2,
|
|
10
|
+
6 => 2,
|
|
11
|
+
7 => 3,
|
|
12
|
+
8 => 3,
|
|
13
|
+
9 => 3
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
LONG_WORD_RATIO = 0.4
|
|
17
|
+
|
|
18
|
+
def self.anchor_length(word_length)
|
|
19
|
+
BUCKET_MAP.fetch(word_length) { (word_length * LONG_WORD_RATIO).ceil }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Jekyll
|
|
2
|
+
module FastReader
|
|
3
|
+
class AssetGenerator < Jekyll::Generator
|
|
4
|
+
priority :low
|
|
5
|
+
|
|
6
|
+
def generate(site)
|
|
7
|
+
config = site.instance_variable_get(:@fast_reader_config)
|
|
8
|
+
return unless config&.enabled?
|
|
9
|
+
|
|
10
|
+
ensure_asset(site, "fast-reader.css")
|
|
11
|
+
ensure_asset(site, "fast-reader.js") if config.toggle
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def ensure_asset(site, filename)
|
|
17
|
+
return if site.static_files.any? { |f| f.is_a?(AssetStaticFile) && f.name == filename }
|
|
18
|
+
|
|
19
|
+
site.static_files << AssetStaticFile.new(site, filename)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Jekyll
|
|
2
|
+
module FastReader
|
|
3
|
+
class AssetStaticFile < Jekyll::StaticFile
|
|
4
|
+
GEM_ROOT = File.expand_path("../../..", __dir__).freeze
|
|
5
|
+
|
|
6
|
+
def initialize(site, filename = "fast-reader.css")
|
|
7
|
+
super(site, GEM_ROOT, "assets", filename)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Jekyll::StaticFile caches source mtimes at the class level (::mtimes).
|
|
11
|
+
# With jekyll-polyglot, multiple language passes share the same process,
|
|
12
|
+
# so the cache entry written by the default-lang pass causes subsequent
|
|
13
|
+
# passes to see modified? == false and skip writing the language-prefixed
|
|
14
|
+
# copy. Always returning true ensures every pass writes its own copy.
|
|
15
|
+
def modified?
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Jekyll
|
|
2
|
+
module FastReader
|
|
3
|
+
# Single source of truth for the character classes shared by the tokenizer
|
|
4
|
+
# and the text processor. Keeping these in one place prevents the regexes
|
|
5
|
+
# from drifting apart (which previously caused inconsistent handling of the
|
|
6
|
+
# left curly quote U+2018).
|
|
7
|
+
module Characters
|
|
8
|
+
# Non-letter characters allowed inside a word: straight apostrophe (\x27),
|
|
9
|
+
# left/right curly quotes (U+2018/U+2019), and hyphen. Written as escapes
|
|
10
|
+
# so the source stays unambiguous in any editor.
|
|
11
|
+
INNER_JOINERS = "\\x27\\u2018\\u2019\\-".freeze
|
|
12
|
+
|
|
13
|
+
# Matches one such joiner, e.g. for stripping them before counting letters.
|
|
14
|
+
INNER_JOINERS_CLASS = /[#{INNER_JOINERS}]/.freeze
|
|
15
|
+
|
|
16
|
+
# Matches a single latin word, which must start and end with a letter and
|
|
17
|
+
# may contain joiners in between.
|
|
18
|
+
WORD = /[a-zA-Z](?:[a-zA-Z#{INNER_JOINERS}]*[a-zA-Z])?/.freeze
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module Jekyll
|
|
2
|
+
module FastReader
|
|
3
|
+
class Configuration
|
|
4
|
+
DEFAULTS = {
|
|
5
|
+
"enabled" => true,
|
|
6
|
+
"collections" => ["posts"],
|
|
7
|
+
"exclude_selectors" => %w[code pre script style kbd samp],
|
|
8
|
+
"css_output_path" => "/assets/fast-reader.css",
|
|
9
|
+
"js_output_path" => "/assets/fast-reader.js",
|
|
10
|
+
"stop_words_extra" => [],
|
|
11
|
+
"toggle" => false,
|
|
12
|
+
"default_on" => true
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :exclude_selectors, :css_output_path, :js_output_path,
|
|
16
|
+
:stop_words_extra, :baseurl, :toggle, :default_on
|
|
17
|
+
|
|
18
|
+
def self.from(site)
|
|
19
|
+
new(site.config["fast_reader"] || {}, site.config["baseurl"].to_s.chomp("/"))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(config, baseurl = "")
|
|
23
|
+
merged = DEFAULTS.merge(config)
|
|
24
|
+
@enabled = merged["enabled"]
|
|
25
|
+
@collections_map = normalize_collections(merged["collections"])
|
|
26
|
+
@exclude_selectors = Array(merged["exclude_selectors"])
|
|
27
|
+
@css_output_path = merged["css_output_path"]
|
|
28
|
+
@js_output_path = merged["js_output_path"]
|
|
29
|
+
@stop_words_extra = Array(merged["stop_words_extra"]).map(&:to_s)
|
|
30
|
+
@toggle = merged["toggle"]
|
|
31
|
+
@default_on = merged["default_on"]
|
|
32
|
+
@baseurl = baseurl
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def enabled?
|
|
37
|
+
@enabled
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def collection_enabled?(label)
|
|
41
|
+
return false unless @collections_map.key?(label)
|
|
42
|
+
|
|
43
|
+
@collections_map[label] != false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def stop_words_for(label)
|
|
47
|
+
options = @collections_map[label]
|
|
48
|
+
return @stop_words_extra unless options.is_a?(Hash)
|
|
49
|
+
|
|
50
|
+
per_collection = Array(options["stop_words_extra"]).map(&:to_s)
|
|
51
|
+
per_collection + @stop_words_extra
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def normalize_collections(value)
|
|
57
|
+
case value
|
|
58
|
+
when Array
|
|
59
|
+
value.each_with_object({}) { |label, h| h[label.to_s] = true }
|
|
60
|
+
when Hash
|
|
61
|
+
value.each_with_object({}) do |(k, v), h|
|
|
62
|
+
h[k.to_s] = normalize_collection_value(v)
|
|
63
|
+
end
|
|
64
|
+
else
|
|
65
|
+
{}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def normalize_collection_value(value)
|
|
70
|
+
return false if value == false
|
|
71
|
+
return value.transform_keys(&:to_s) if value.is_a?(Hash)
|
|
72
|
+
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Jekyll
|
|
2
|
+
module FastReader
|
|
3
|
+
class DocumentFilter
|
|
4
|
+
def initialize(config)
|
|
5
|
+
@config = config
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def process?(document)
|
|
9
|
+
return false unless @config.enabled?
|
|
10
|
+
|
|
11
|
+
override = front_matter_override(document)
|
|
12
|
+
return override unless override.nil?
|
|
13
|
+
|
|
14
|
+
in_configured_collection?(document)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def front_matter_override(document)
|
|
20
|
+
return nil unless document.respond_to?(:data)
|
|
21
|
+
|
|
22
|
+
value = document.data["fast_reader"]
|
|
23
|
+
return nil unless value == true || value == false
|
|
24
|
+
|
|
25
|
+
value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def in_configured_collection?(document)
|
|
29
|
+
return false unless document.respond_to?(:collection)
|
|
30
|
+
return false if document.collection.nil?
|
|
31
|
+
|
|
32
|
+
@config.collection_enabled?(document.collection.label)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Jekyll
|
|
2
|
+
module FastReader
|
|
3
|
+
module Hooks
|
|
4
|
+
def self.register!
|
|
5
|
+
Jekyll::Hooks.register :site, :after_init do |site|
|
|
6
|
+
config = Configuration.from(site)
|
|
7
|
+
site.instance_variable_set(:@fast_reader_config, config)
|
|
8
|
+
site.instance_variable_set(:@fast_reader_filter, DocumentFilter.new(config))
|
|
9
|
+
site.instance_variable_set(:@fast_reader_transformer, Transformer.new(config))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
%i[documents pages].each do |scope|
|
|
13
|
+
Jekyll::Hooks.register scope, :post_render do |renderable|
|
|
14
|
+
apply_transform(renderable)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.apply_transform(renderable)
|
|
20
|
+
site = renderable.site
|
|
21
|
+
config = site.instance_variable_get(:@fast_reader_config)
|
|
22
|
+
filter = site.instance_variable_get(:@fast_reader_filter)
|
|
23
|
+
transformer = site.instance_variable_get(:@fast_reader_transformer)
|
|
24
|
+
return unless config && filter && transformer && filter.process?(renderable)
|
|
25
|
+
|
|
26
|
+
label = renderable.collection&.label if renderable.respond_to?(:collection)
|
|
27
|
+
stop_words = label ? config.stop_words_for(label) : config.stop_words_extra
|
|
28
|
+
renderable.output = transformer.call(renderable.output, stop_words: stop_words)
|
|
29
|
+
end
|
|
30
|
+
private_class_method :apply_transform
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Jekyll
|
|
2
|
+
module FastReader
|
|
3
|
+
class HtmlWalker
|
|
4
|
+
def initialize(exclude_selectors)
|
|
5
|
+
@exclude_selectors = exclude_selectors
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def walk(root, &block)
|
|
9
|
+
return if element_excluded?(root)
|
|
10
|
+
|
|
11
|
+
root.children.each do |node|
|
|
12
|
+
if node.text?
|
|
13
|
+
block.call(node) unless trails_anchor?(node)
|
|
14
|
+
elsif node.element?
|
|
15
|
+
walk(node, &block)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# The text immediately following an fr-anchor span is the unbolded tail of
|
|
23
|
+
# an already-processed word. Re-processing it would re-anchor that tail, so
|
|
24
|
+
# it is left alone. This keeps the transform idempotent and stops the
|
|
25
|
+
# fast_reader Liquid filter from colliding with the automatic pass.
|
|
26
|
+
def trails_anchor?(node)
|
|
27
|
+
previous = node.previous_sibling
|
|
28
|
+
previous&.element? && already_anchored?(previous)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def element_excluded?(node)
|
|
32
|
+
return false unless node.respond_to?(:element?) && node.element?
|
|
33
|
+
return true if node.respond_to?(:[]) && node["data-fr-skip"]
|
|
34
|
+
return true if already_anchored?(node)
|
|
35
|
+
|
|
36
|
+
@exclude_selectors.any? do |selector|
|
|
37
|
+
node.matches?(selector)
|
|
38
|
+
rescue Nokogiri::CSS::SyntaxError
|
|
39
|
+
Jekyll.logger.warn "FastReader:", "Invalid CSS selector ignored: #{selector.inspect}"
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Already-anchored spans (e.g. emitted by the fast_reader Liquid filter)
|
|
45
|
+
# must not be descended into, otherwise a second pass nests fr-anchor spans.
|
|
46
|
+
def already_anchored?(node)
|
|
47
|
+
return false unless node.respond_to?(:[])
|
|
48
|
+
|
|
49
|
+
node["class"].to_s.split.include?("fr-anchor")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require "liquid"
|
|
2
|
+
require "nokogiri"
|
|
3
|
+
|
|
4
|
+
module Jekyll
|
|
5
|
+
module FastReader
|
|
6
|
+
module LiquidFilters
|
|
7
|
+
WORDS_PER_MINUTE = 250
|
|
8
|
+
|
|
9
|
+
def reading_time(input)
|
|
10
|
+
minutes = (fr_word_count(input).to_f / WORDS_PER_MINUTE).ceil
|
|
11
|
+
minutes = 1 if minutes < 1
|
|
12
|
+
"#{minutes} min"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def word_count(input)
|
|
16
|
+
fr_word_count(input)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def fast_reader(input)
|
|
20
|
+
config = fr_site_config
|
|
21
|
+
processor = TextProcessor.new(StopWords.new(config.stop_words_extra))
|
|
22
|
+
processor.process(input.to_s)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def fr_word_count(input)
|
|
28
|
+
Nokogiri::HTML.fragment(input.to_s).text.scan(/\S+/).length
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def fr_site_config
|
|
32
|
+
site = @context && @context.registers && @context.registers[:site]
|
|
33
|
+
site&.instance_variable_get(:@fast_reader_config) || Configuration.new({})
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Liquid::Template.register_filter(Jekyll::FastReader::LiquidFilters)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module FastReader
|
|
5
|
+
class StopWords
|
|
6
|
+
DEFAULT = Set.new(%w[
|
|
7
|
+
a an the of in on at to by or is as
|
|
8
|
+
and but for nor so yet
|
|
9
|
+
it its this that
|
|
10
|
+
]).freeze
|
|
11
|
+
|
|
12
|
+
def initialize(extra = [])
|
|
13
|
+
@words = DEFAULT | Set.new(extra.map(&:downcase))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def stop?(word)
|
|
17
|
+
@words.include?(word.downcase)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "cgi"
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module FastReader
|
|
5
|
+
class TextProcessor
|
|
6
|
+
def initialize(stop_words)
|
|
7
|
+
@stop_words = stop_words
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def process(text)
|
|
11
|
+
Tokenizer.tokenize(text).map { |type, value| transform(type, value) }.join
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def transform(type, value)
|
|
17
|
+
return CGI.escape_html(value) if type == :punctuation
|
|
18
|
+
return value unless type == :word
|
|
19
|
+
return value if @stop_words.stop?(value)
|
|
20
|
+
|
|
21
|
+
value.include?("-") ? wrap_hyphenated(value) : wrap_simple(value)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def wrap_hyphenated(word)
|
|
25
|
+
word.split("-").map { |part| wrap_simple(part) }.join("-")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def wrap_simple(word)
|
|
29
|
+
letters = word.gsub(Characters::INNER_JOINERS_CLASS, "")
|
|
30
|
+
return CGI.escape_html(word) if letters.empty?
|
|
31
|
+
|
|
32
|
+
anchor_len = AnchorCalculator.anchor_length(letters.length)
|
|
33
|
+
split_at = find_split_position(word, anchor_len)
|
|
34
|
+
anchor = word[0...split_at]
|
|
35
|
+
rest = word[split_at..]
|
|
36
|
+
|
|
37
|
+
return CGI.escape_html(word) if rest.nil? || rest.empty?
|
|
38
|
+
|
|
39
|
+
%(<span class="fr-anchor">#{CGI.escape_html(anchor)}</span>#{CGI.escape_html(rest)})
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find_split_position(word, anchor_len)
|
|
43
|
+
letter_count = 0
|
|
44
|
+
word.each_char.with_index do |char, i|
|
|
45
|
+
letter_count += 1 if char.match?(/[a-zA-Z]/)
|
|
46
|
+
return i + 1 if letter_count >= anchor_len
|
|
47
|
+
end
|
|
48
|
+
word.length
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Jekyll
|
|
2
|
+
module FastReader
|
|
3
|
+
class Tokenizer
|
|
4
|
+
PATTERN = /(#{Characters::WORD})|(\s+)|([^\sa-zA-Z]+)/
|
|
5
|
+
|
|
6
|
+
def self.tokenize(text)
|
|
7
|
+
text.scan(PATTERN).map do |word, whitespace, punctuation|
|
|
8
|
+
if word
|
|
9
|
+
[:word, word]
|
|
10
|
+
elsif whitespace
|
|
11
|
+
[:whitespace, whitespace]
|
|
12
|
+
else
|
|
13
|
+
[:punctuation, punctuation]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require "nokogiri"
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module FastReader
|
|
5
|
+
class Transformer
|
|
6
|
+
def initialize(config)
|
|
7
|
+
@config = config
|
|
8
|
+
@walker = HtmlWalker.new(config.exclude_selectors)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(html, stop_words: nil)
|
|
12
|
+
return html unless html =~ /<body\b/i
|
|
13
|
+
|
|
14
|
+
had_head = html.include?("</head>")
|
|
15
|
+
had_body = html.include?("</body>")
|
|
16
|
+
|
|
17
|
+
doc = Nokogiri::HTML(html)
|
|
18
|
+
body = doc.at_css("body")
|
|
19
|
+
return html unless body
|
|
20
|
+
|
|
21
|
+
words = stop_words || @config.stop_words_extra
|
|
22
|
+
processor = TextProcessor.new(StopWords.new(words))
|
|
23
|
+
|
|
24
|
+
text_nodes = []
|
|
25
|
+
@walker.walk(body) { |node| text_nodes << node }
|
|
26
|
+
text_nodes.each { |node| node.replace(processor.process(node.content)) }
|
|
27
|
+
|
|
28
|
+
inject_stylesheet(doc) if had_head
|
|
29
|
+
inject_script(doc) if had_head && @config.toggle
|
|
30
|
+
inject_body_class(body, "fr-opt-in") unless @config.default_on
|
|
31
|
+
inject_toggle(body) if had_body && @config.toggle
|
|
32
|
+
|
|
33
|
+
doc.to_html
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def inject_stylesheet(doc)
|
|
39
|
+
head = doc.at_css("head")
|
|
40
|
+
return unless head
|
|
41
|
+
|
|
42
|
+
href = "#{@config.baseurl}#{@config.css_output_path}"
|
|
43
|
+
return if head.css('link[rel="stylesheet"]').any? { |l| l["href"] == href }
|
|
44
|
+
|
|
45
|
+
link = Nokogiri::XML::Node.new("link", doc)
|
|
46
|
+
link["rel"] = "stylesheet"
|
|
47
|
+
link["href"] = href
|
|
48
|
+
head.add_child(link)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def inject_script(doc)
|
|
52
|
+
head = doc.at_css("head")
|
|
53
|
+
return unless head
|
|
54
|
+
|
|
55
|
+
src = "#{@config.baseurl}#{@config.js_output_path}"
|
|
56
|
+
return if head.css("script").any? { |s| s["src"] == src }
|
|
57
|
+
|
|
58
|
+
script = Nokogiri::XML::Node.new("script", doc)
|
|
59
|
+
script["src"] = src
|
|
60
|
+
script["defer"] = "defer"
|
|
61
|
+
head.add_child(script)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def inject_body_class(body, class_name)
|
|
65
|
+
classes = body["class"].to_s.split
|
|
66
|
+
return if classes.include?(class_name)
|
|
67
|
+
|
|
68
|
+
body["class"] = (classes + [class_name]).join(" ")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def inject_toggle(body)
|
|
72
|
+
return if body.at_css("#fr-toggle")
|
|
73
|
+
|
|
74
|
+
button = Nokogiri::XML::Node.new("button", body.document)
|
|
75
|
+
button["id"] = "fr-toggle"
|
|
76
|
+
button["type"] = "button"
|
|
77
|
+
button["aria-label"] = "Toggle Fast Reader"
|
|
78
|
+
button["aria-pressed"] = @config.default_on ? "true" : "false"
|
|
79
|
+
button["aria-keyshortcuts"] = "Alt+Shift+B"
|
|
80
|
+
button["data-fr-mode"] = @config.default_on ? "default-on" : "opt-in"
|
|
81
|
+
button.content = "Fast Reader"
|
|
82
|
+
body.add_child(button)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "jekyll"
|
|
2
|
+
require "nokogiri"
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
require_relative "jekyll/fast_reader/version"
|
|
7
|
+
require_relative "jekyll/fast_reader/configuration"
|
|
8
|
+
require_relative "jekyll/fast_reader/stop_words"
|
|
9
|
+
require_relative "jekyll/fast_reader/characters"
|
|
10
|
+
require_relative "jekyll/fast_reader/anchor_calculator"
|
|
11
|
+
require_relative "jekyll/fast_reader/tokenizer"
|
|
12
|
+
require_relative "jekyll/fast_reader/text_processor"
|
|
13
|
+
require_relative "jekyll/fast_reader/html_walker"
|
|
14
|
+
require_relative "jekyll/fast_reader/transformer"
|
|
15
|
+
require_relative "jekyll/fast_reader/document_filter"
|
|
16
|
+
require_relative "jekyll/fast_reader/asset_static_file"
|
|
17
|
+
require_relative "jekyll/fast_reader/asset_generator"
|
|
18
|
+
require_relative "jekyll/fast_reader/hooks"
|
|
19
|
+
require_relative "jekyll/fast_reader/liquid_filters"
|
|
20
|
+
|
|
21
|
+
Jekyll::FastReader::Hooks.register!
|
metadata
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jekyll-fast-reader
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- developerlee79
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-04 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: jekyll
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '4.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '5.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '4.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: nokogiri
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 1.15.6
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '2.0'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: 1.15.6
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '2.0'
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: rspec
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '3.12'
|
|
60
|
+
type: :development
|
|
61
|
+
prerelease: false
|
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3.12'
|
|
67
|
+
- !ruby/object:Gem::Dependency
|
|
68
|
+
name: rake
|
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '13.0'
|
|
74
|
+
type: :development
|
|
75
|
+
prerelease: false
|
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '13.0'
|
|
81
|
+
- !ruby/object:Gem::Dependency
|
|
82
|
+
name: simplecov
|
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0.22'
|
|
88
|
+
type: :development
|
|
89
|
+
prerelease: false
|
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '0.22'
|
|
95
|
+
description: Transforms Jekyll post HTML by bolding the initial characters of each
|
|
96
|
+
word, optimised for fast visual reading. Toggle on/off via a CSS class.
|
|
97
|
+
email:
|
|
98
|
+
- developerlee79@users.noreply.github.com
|
|
99
|
+
executables: []
|
|
100
|
+
extensions: []
|
|
101
|
+
extra_rdoc_files: []
|
|
102
|
+
files:
|
|
103
|
+
- LICENSE.txt
|
|
104
|
+
- README.md
|
|
105
|
+
- assets/fast-reader.css
|
|
106
|
+
- assets/fast-reader.js
|
|
107
|
+
- lib/jekyll-fast-reader.rb
|
|
108
|
+
- lib/jekyll/fast_reader/anchor_calculator.rb
|
|
109
|
+
- lib/jekyll/fast_reader/asset_generator.rb
|
|
110
|
+
- lib/jekyll/fast_reader/asset_static_file.rb
|
|
111
|
+
- lib/jekyll/fast_reader/characters.rb
|
|
112
|
+
- lib/jekyll/fast_reader/configuration.rb
|
|
113
|
+
- lib/jekyll/fast_reader/document_filter.rb
|
|
114
|
+
- lib/jekyll/fast_reader/hooks.rb
|
|
115
|
+
- lib/jekyll/fast_reader/html_walker.rb
|
|
116
|
+
- lib/jekyll/fast_reader/liquid_filters.rb
|
|
117
|
+
- lib/jekyll/fast_reader/stop_words.rb
|
|
118
|
+
- lib/jekyll/fast_reader/text_processor.rb
|
|
119
|
+
- lib/jekyll/fast_reader/tokenizer.rb
|
|
120
|
+
- lib/jekyll/fast_reader/transformer.rb
|
|
121
|
+
- lib/jekyll/fast_reader/version.rb
|
|
122
|
+
homepage: https://github.com/developerlee79/jekyll-fast-reader
|
|
123
|
+
licenses:
|
|
124
|
+
- MIT
|
|
125
|
+
metadata:
|
|
126
|
+
homepage_uri: https://github.com/developerlee79/jekyll-fast-reader
|
|
127
|
+
source_code_uri: https://github.com/developerlee79/jekyll-fast-reader
|
|
128
|
+
changelog_uri: https://github.com/developerlee79/jekyll-fast-reader/blob/main/CHANGELOG.md
|
|
129
|
+
post_install_message:
|
|
130
|
+
rdoc_options: []
|
|
131
|
+
require_paths:
|
|
132
|
+
- lib
|
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: 2.7.0
|
|
138
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
139
|
+
requirements:
|
|
140
|
+
- - ">="
|
|
141
|
+
- !ruby/object:Gem::Version
|
|
142
|
+
version: '0'
|
|
143
|
+
requirements: []
|
|
144
|
+
rubygems_version: 3.1.6
|
|
145
|
+
signing_key:
|
|
146
|
+
specification_version: 4
|
|
147
|
+
summary: Jekyll plugin for accelerated reading via visual word anchoring
|
|
148
|
+
test_files: []
|