monoholic 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ee37aea1bd4ecf60b33567d582c4363961f49364bd2176f42e8b0de8aaa12ce
4
- data.tar.gz: a00389e09ed54a0d268694fc002614cd0eca0820555d6f888d4442d22e353a34
3
+ metadata.gz: 0b2feef2d72bb935b6e4e6bdb2d5c39034616dadd12f3c6ca9158a43cafe24d8
4
+ data.tar.gz: 354b3cc49d0afcbcfe40e21f2e0da506310c34b3ee44a06392a397d8e90360a4
5
5
  SHA512:
6
- metadata.gz: 47a614360018402b6e397fb4e8f6d16cb267919176a479dbb3c6ac58c68d4c605af839e84379fc84eca2ffb36ab77ab77c546521cd5e77cb3366ac1e561fa868
7
- data.tar.gz: cd0c4518e8d0e39ddfe00c56a8311c63d375c5ee749313e15be1ad4a29316ec36321fd4219b2900b7475b9287a5e57811e8f2c685a4f3d2fbfeec85a8ffde0be
6
+ metadata.gz: 3acf3815b503469a52aab55448404d6c2f3869fb636c11831c4d698d962510558b1d6f282f749fe51b8e923c9c29ed9e9ed56705f1ea6d28c5a940f5f3c57a21
7
+ data.tar.gz: 929ca0d46b670294db583b75f4e633bbe2eabc990cd916b360694159ac357a689bcf3409fffc9f7e06b6d5cbc930e65e1b50010cc4dc6d16642022169eb2091f
data/README.md CHANGED
@@ -40,26 +40,69 @@ $ bundle install
40
40
 
41
41
  ### Manual Installation (GitHub Pages)
42
42
 
43
- If you're running Jekyll without RubyGems or prefer to use GitHub Pages remote themes, update your `_config.yml`:
43
+ For GitHub's built-in Jekyll build, add the following to `_config.yml`:
44
44
 
45
45
  ```yaml
46
- remote_theme: stiermid/monoholic
46
+ title: "Your site"
47
+ description: "A short description of your site."
48
+ remote_theme: stiermid/monoholic@v1.4.0
49
+ url: "https://<user>.github.io"
50
+ baseurl: "/your-repo"
47
51
  plugins:
48
52
  - jekyll-remote-theme
49
53
  - jekyll-feed
50
54
  - jekyll-seo-tag
55
+ - jekyll-sitemap
51
56
  ```
52
57
 
53
- `jekyll-remote-theme` fetches the theme on Pages; `jekyll-feed` and `jekyll-seo-tag` are required by the theme's `head.html` (`{% feed_meta %}`, `{% seo %}`). For project sites, also set `baseurl` (e.g. `/your-repo`) and `url` (e.g. `https://<user>.github.io`). To pin a release: `remote_theme: stiermid/monoholic@v1.2.0`.
58
+ Replace the user and repository placeholders. Use `baseurl: ""` for a user
59
+ site or a custom domain served at its root. Pinning a release keeps theme
60
+ updates explicit; omit `@v1.4.0` only if you want the repository's default
61
+ branch instead.
54
62
 
55
- > **Pages note:** Native GitHub Pages builds run Jekyll 3.9. The `monoholic` RubyGem requires Jekyll 4.4, so on GitHub Pages use `remote_theme` only (not `theme:` + `gem "monoholic"`). SCSS intentionally uses `@import` for 3.9 compatibility.
63
+ `jekyll-remote-theme` loads the theme. Feed and SEO tags require
64
+ `jekyll-feed` and `jekyll-seo-tag`; `jekyll-sitemap` generates the sitemap
65
+ and a default `robots.txt` when the site does not supply one.
56
66
 
57
- | Install method | Jekyll | How |
58
- | -------------------------------------------------------- | ------------- | --------------------------- |
59
- | RubyGems (`theme: monoholic`) | 4.4 | local / CI / non-Pages host |
60
- | GitHub Pages (`remote_theme: stiermid/monoholic@v1.2.0`) | 3.9 via Pages | Pages only |
67
+ Create a homepage, such as `index.md`:
61
68
 
62
- Or, simply fork this repository, adapt the `_config.yml` according to your needs, and you're good to go!
69
+ ```markdown
70
+ ---
71
+ layout: home
72
+ ---
73
+
74
+ Welcome to my site.
75
+ ```
76
+
77
+ Also create `_data/menu.yml` in your site:
78
+
79
+ ```yaml
80
+ entries:
81
+ - title: all posts
82
+ post_list: true
83
+ ```
84
+
85
+ Native Jekyll 3.10 does not inherit a remote theme's `_data` directory, so
86
+ your site must supply its own menu. Add your posts under `_posts/` and
87
+ extend the menu using the options below.
88
+
89
+ > **Pages note:** Native Pages currently uses Jekyll 3.10.0 and Sass converter
90
+ > 1.5.2 with Ruby Sass. The `monoholic` gem requires Jekyll 4.4, so do not
91
+ > combine it with the native Pages bundle. Use `remote_theme` instead of
92
+ > `theme: monoholic` for that build. SCSS retains `@import` for this older
93
+ > Sass compiler.
94
+
95
+ | Method | Jekyll | Build |
96
+ | ------------ | ------ | ------------------- |
97
+ | RubyGem | 4.4 | Local / custom CI |
98
+ | Remote theme | 3.10 | Native GitHub Pages |
99
+
100
+ You can still deploy a Jekyll 4.4 gem-based site to Pages as static output
101
+ from a custom build workflow. The version restriction applies to GitHub's
102
+ built-in Jekyll build, not to static hosting.
103
+
104
+ For a full source checkout instead, fork this repository and adapt
105
+ `_config.yml` to your site.
63
106
 
64
107
  ## Usage
65
108
 
@@ -67,6 +110,8 @@ Or, simply fork this repository, adapt the `_config.yml` according to your needs
67
110
 
68
111
  Once installed, build your site using the provided layouts (`default`, `home`, `page`, `post`).
69
112
 
113
+ On pages using the `home` layout, page content appears before the sections defined in `_data/menu.yml`.
114
+
70
115
  ### Configuration
71
116
 
72
117
  Override the default settings in your `_config.yml`. Key theme configuration options:
@@ -77,8 +122,35 @@ theme_config:
77
122
  date_format: "%Y-%m-%d" # Date format for post metadata
78
123
  monochrome_images: true # Apply grayscale filter to images (default: true)
79
124
  footer: true # Show/hide site footer (default: true)
125
+ skip_link_label: "Skip to content" # Keyboard skip-link text
126
+ site_sections_label: "Site sections" # Accessible name of the homepage navigation
127
+ ```
128
+
129
+ ### Language and Interface Labels
130
+
131
+ The document's `<html lang>` uses the page's `lang` front matter, then the site's `lang` setting, then `en`. Use BCP 47 language tags such as `de`, `en-GB`, or `zh-Hant-TW`. Surrounding whitespace is trimmed; missing, empty, whitespace-only, or `false` values fall back to the next setting.
132
+
133
+ For example, configure a German site in `_config.yml`:
134
+
135
+ ```yaml
136
+ lang: de
137
+ theme_config:
138
+ skip_link_label: "Zum Inhalt"
139
+ site_sections_label: "Seitenbereiche"
140
+ back: "Zurück"
141
+ ```
142
+
143
+ Override the document language for an individual page or post in its front matter:
144
+
145
+ ```yaml
146
+ ---
147
+ layout: page
148
+ lang: fr-CA
149
+ ---
80
150
  ```
81
151
 
152
+ Declaring `lang` helps browsers and assistive technology interpret your content; it does not translate the content or interface automatically. Interface labels above are site-wide and rendered as plain text, not HTML. Missing or blank labels retain their English defaults so the skip link and navigation keep accessible names. Existing scroll labels and hints can also be translated under [Responsive Content](#responsive-content).
153
+
82
154
  ### Fediverse Creator tag
83
155
 
84
156
  Monoholic supports `fediverse:creator` meta tag:
@@ -113,9 +185,12 @@ entries:
113
185
 
114
186
  - `title`: The section header.
115
187
  - `content`: Custom HTML or text for the menu section.
116
- - `content_file`: Path to an external markdown file (e.g., `about.md`) to load content from your root directory.
188
+ - `content_include`: Path relative to `_includes/` for a Markdown fragment with Liquid support (recommended for reusable content).
189
+ - `content_file`: Path to an existing Jekyll page (e.g., `about.md`) relative to your site root. Retained for compatibility; does not reliably evaluate Liquid.
117
190
  - `post_list`: Set to `true` to auto-generate a list of your Jekyll posts under this section.
118
191
 
192
+ Use one content source per entry. If several are supplied, precedence is `content_include`, then `content_file`, then `content`.
193
+
119
194
  #### Nested Menu Sections
120
195
 
121
196
  Menu entries can contain nested `entries` to create recursive sub-sections:
@@ -133,9 +208,40 @@ entries:
133
208
 
134
209
  Nesting can go as deep as needed — `menu.html` includes itself recursively for each `entries` array it encounters.
135
210
 
211
+ Top-level titles use `h2`. Each titled parent advances its children by one heading level, capped at `h6`. Entries with missing, empty, or whitespace-only titles are unlabelled groups: they render no heading and do not advance the heading level. Later siblings always retain their own level, regardless of the depth of earlier branches.
212
+
213
+ Each titled section is labelled by its heading. Heading IDs include the ancestor path and sibling index, so repeated titles remain distinct. When including multiple copies of a menu on the same page, give each copy a distinct `id_prefix` to avoid overlapping IDs:
214
+
215
+ ```liquid
216
+ {% include menu.html collection=site.data.menu.entries level=2 id_prefix='primary' %}
217
+ {% include menu.html collection=site.data.menu.entries level=2 id_prefix='secondary' %}
218
+ ```
219
+
136
220
  #### Using External Content Files
137
221
 
138
- You can reference external markdown files instead of inline content:
222
+ For reusable content containing Liquid, create a Markdown fragment in your site's `_includes/` directory, for example `_includes/fragments/about.md`:
223
+
224
+ ```liquid
225
+ Welcome to **{{ site.title }}**.
226
+
227
+ [Home]({{ '/' | relative_url }})
228
+ ```
229
+
230
+ Reference it using `content_include`:
231
+
232
+ ```yaml
233
+ entries:
234
+ - title: about
235
+ content_include: fragments/about.md
236
+ ```
237
+
238
+ Jekyll renders Liquid first, then Monoholic converts the resulting Markdown to HTML once. Standard Liquid variables, filters, tags, and nested includes work without a custom plugin, including in safe mode. `page` refers to the page displaying the menu, not the fragment. Use `relative_url` for links that must work on project sites such as `/blog`.
239
+
240
+ Fragments must **not** have YAML front matter. They do not generate standalone pages. Paths are relative to `_includes/`, even when the menu appears on a nested page; missing files and invalid paths fail the build with Jekyll's include error rather than silently omitting content. Only include trusted, author-controlled files.
241
+
242
+ ##### Existing `content_file` references
243
+
244
+ Existing page references remain supported:
139
245
 
140
246
  ```yaml
141
247
  entries:
@@ -143,7 +249,17 @@ entries:
143
249
  content_file: about.md
144
250
  ```
145
251
 
146
- This will load and render the content from `about.md` in your site's root directory.
252
+ The referenced file must be a Jekyll page with YAML front matter (an empty `---` block is sufficient) and must not be excluded from the build. Its Markdown content is embedded without its layout; the page can also generate its own standalone URL. Missing page references are silently omitted, preserving existing behavior.
253
+
254
+ `content_file` does not run a separate Liquid rendering pass. Results for Liquid expressions can depend on page rendering order, so use `content_include` whenever the content contains Liquid.
255
+
256
+ To migrate, move reusable content from `about.md` to `_includes/fragments/about.md`, remove its YAML front matter, and replace `content_file: about.md` with `content_include: fragments/about.md`. If you still need a standalone About page, keep `about.md` with its front matter and include the fragment in its body:
257
+
258
+ ```liquid
259
+ {% include fragments/about.md %}
260
+ ```
261
+
262
+ Use a Markdown page for this wrapper so Jekyll converts the included Markdown normally. Remove an old standalone page only if you no longer need its URL.
147
263
 
148
264
  ### Favicon
149
265
 
@@ -171,6 +287,27 @@ Note: the theme uses `@import` (not `@use`) to stay compatible with GitHub Pages
171
287
 
172
288
  To override CSS custom properties (colors, spacing, fonts) without touching SCSS, redeclare the variables in your own stylesheet — they are exposed under `:root` in `_sass/_variables.scss`.
173
289
 
290
+ ### Responsive Content
291
+
292
+ Body text defaults to `1rem`, following the browser's default font size
293
+ instead of fixing it at 16 pixels. When overriding `--font-size-base`, use
294
+ a relative value such as `1rem` or `1.125rem` to preserve that preference.
295
+
296
+ Long words, URLs, and inline code wrap when necessary. Plain and highlighted code blocks preserve indentation and source lines, scrolling horizontally inside the block instead of expanding the page.
297
+
298
+ The small, dependency-free `assets/js/content-scroll.js` script progressively enhances tables with scroll containers while preserving native table semantics. Overflowing tables and code blocks receive a keyboard tab stop, an accessible name, and a visible scrolling hint. These are updated when the viewport, content dimensions, or fonts change; fitting content does not add automatic tab stops.
299
+
300
+ Table captions are used as accessible names when available. Existing author-provided labels, descriptions, and tab indices are preserved. You can translate the default labels and hint in `_config.yml`:
301
+
302
+ ```yaml
303
+ theme_config:
304
+ table_scroll_label: "Table"
305
+ code_scroll_label: "Code block"
306
+ scroll_hint: "Scroll horizontally to see more."
307
+ ```
308
+
309
+ Without JavaScript, tables use fixed-layout, wrapping cells, and code blocks still scroll. For especially wide tables that should scroll even without JavaScript, supply a `.table-scroll` wrapper with `tabindex="0"`, `role="region"`, and an accessible name (`aria-label` or `aria-labelledby`). The enhancement reuses that wrapper rather than nesting another one.
310
+
174
311
  ### Per-Page JavaScript
175
312
 
176
313
  To load custom JavaScript files on a specific page, add a `custom_js` list to the page's front matter:
@@ -188,6 +325,115 @@ Each entry is a filename (without `.js`) relative to `assets/js/`. The example a
188
325
 
189
326
  ## Contributing
190
327
 
328
+ ### Running Tests
329
+
330
+ ```sh
331
+ bundle install
332
+ npm ci
333
+ npx playwright install --with-deps chromium firefox webkit
334
+ bundle exec ruby _tests/release_workflow_test.rb
335
+ bundle exec ruby _tests/release_tag_test.rb
336
+ bundle exec ruby _tests/home_layout_test.rb
337
+ bundle exec ruby _tests/localization_test.rb
338
+ JEKYLL_SAFE=1 bundle exec ruby _tests/localization_test.rb
339
+ bundle exec ruby _tests/menu_layout_test.rb
340
+ JEKYLL_SAFE=1 bundle exec ruby _tests/menu_layout_test.rb
341
+ bundle exec ruby _tests/menu_content_test.rb
342
+ JEKYLL_SAFE=1 bundle exec ruby _tests/menu_content_test.rb
343
+ npm run test:browser
344
+ ```
345
+
346
+ Browser tests build and serve an isolated fixture site. They cover Chromium, Firefox, and WebKit at 320px, 375px, 768px, and 1280px, including long text, plain and highlighted code, Markdown and HTML tables, keyboard scrolling, resizing, and JavaScript-disabled layouts. External fonts are blocked so the tests do not depend on Google Fonts being available.
347
+
348
+ Text-sizing checks enlarge the root font to 200% at each viewport width,
349
+ with and without JavaScript. A Firefox-specific test also changes the
350
+ actual browser default-font preference to 32 pixels. These are focused
351
+ regression checks, not a complete WCAG conformance audit.
352
+
353
+ Menu rendering checks cover branching recursion, deep nesting, untitled groups, repeated titles, unique IDs, escaped labels, and independent menu instances in normal and Jekyll safe mode. Browser tests also verify native heading and section semantics with JavaScript disabled.
354
+
355
+ Menu content checks build root and `/blog` sites in normal and safe mode. They cover Liquid-before-Markdown rendering, nested includes and host pages, recursive sections, literal Liquid examples, legacy content sources, and invalid include paths.
356
+
357
+ Localization checks cover language inheritance across all four layouts, page overrides, blank-value fallbacks, escaped attributes, translated labels, and unchanged skip-link destinations in normal and safe mode.
358
+
359
+ To run one browser, use `npm run test:browser -- --project=chromium`. You can also set `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` to use a locally installed Chromium binary. Generated sites, screenshots, and traces are ignored by Git; CI retains failure diagnostics for seven days.
360
+
361
+ ### Native GitHub Pages Checks
362
+
363
+ The Pages tests use a separate Gemfile and lockfile so the theme's Jekyll 4.4
364
+ dependency cannot affect the native Pages graph. CI runs them on Ruby 3.3.
365
+ Install and run them with:
366
+
367
+ ```sh
368
+ BUNDLE_GEMFILE=_tests/pages/Gemfile bundle install
369
+ BUNDLE_GEMFILE=_tests/pages/Gemfile bundle exec \
370
+ ruby _tests/pages/consumer_test.rb
371
+ ```
372
+
373
+ This bundle pins GitHub Pages 232, Jekyll 3.10.0, Sass converter 1.5.2, and
374
+ the other published Pages dependencies. The test invokes the real Pages
375
+ build command with its safe-mode configuration and default plugins. It
376
+ checks root and `/blog` sites, layouts, menu data, nested Liquid includes,
377
+ recursive headings, language settings, compiled CSS, scripts, favicons,
378
+ feeds, sitemap URLs, and rejection of a broken theme archive.
379
+
380
+ Only the theme-download HTTP response is supplied locally, using a ZIP of
381
+ the checkout. The real `jekyll-remote-theme` plugin downloads, extracts, and
382
+ loads it. No live GitHub requests or installed `monoholic` gem are used.
383
+ The generated lockfile retains Bundler's checksum format.
384
+
385
+ Review the [published dependency matrix](https://pages.github.com/versions/)
386
+ when updating this bundle. Update its Gemfile, lockfile, and expected versions
387
+ in `_tests/pages/consumer_test.rb` together.
388
+
389
+ ### Release Validation
390
+
391
+ Pull requests and release-tag pushes run gem checks on Ruby 3.3 and 3.4,
392
+ plus the separate native Pages check on Ruby 3.3. Publishing requires all
393
+ three jobs to succeed and is restricted to pushed `v*` tags; pull requests
394
+ never publish. The tag must be `v` followed by the exact gem version in
395
+ `monoholic.gemspec`, including any RubyGems prerelease suffix. Update the
396
+ version metadata and lockfiles together before tagging.
397
+
398
+ Each gem-validation job builds the gem once and installs it into a temporary
399
+ gem home. A clean consumer's Gemfile declares only `monoholic`; Bundler resolves
400
+ its runtime dependencies from installed gems without downloading gems or
401
+ falling back to repository theme files. The checks run
402
+ `bundle exec jekyll build` for root and `/blog` sites and verify layouts,
403
+ compiled CSS, scripts, favicons, feeds, and URLs. Negative tests reject broken
404
+ packages and invalid versions. ERB must be declared as a runtime dependency,
405
+ even on hosts that provide it as a default gem; this is checked on both Rubies.
406
+
407
+ Consumer builds use resolved Ruby and gem executable paths, with no Jekyll
408
+ wrapper available on `PATH`. This also covers vendored CI installations
409
+ without relying on globally installed gem executables.
410
+
411
+ After validation, the Ruby 3.3 gem job uploads its gem and SHA-256 manifest
412
+ as a seven-day artifact; the other jobs do not upload a competing release
413
+ artifact. Once both gem jobs and native Pages pass, publishing downloads
414
+ those same bytes, verifies the checksum and package name/version, and pushes
415
+ the same gem to GitHub Packages and RubyGems—without checking out or rebuilding
416
+ the source. Rerunning validation replaces the artifact only after checks
417
+ succeed; if it has expired, rerun validation rather than just publishing.
418
+
419
+ Publishing keeps the existing authentication: `GITHUB_TOKEN` with package-write permission for GitHub Packages, and the repository's `RUBYGEMS_API_KEY` secret for RubyGems. Validation has read-only repository permissions; registry credentials are used only by publishing steps.
420
+
421
+ To exercise package checks locally without publishing (publisher verification requires Bash and `sha256sum` from GNU coreutils, matching the Linux CI runner):
422
+
423
+ ```sh
424
+ mkdir -p _tests/.release
425
+ bundle exec gem build monoholic.gemspec --output _tests/.release/monoholic.gem
426
+ bundle exec ruby _tests/package_consumer_test.rb _tests/.release/monoholic.gem
427
+ bundle exec ruby _tests/package_consumer_negative_test.rb _tests/.release/monoholic.gem
428
+ (
429
+ cd _tests/.release
430
+ sha256sum monoholic.gem > monoholic.gem.sha256
431
+ )
432
+ bundle exec ruby _tests/package_integrity_test.rb _tests/.release/monoholic.gem _tests/.release/monoholic.gem.sha256
433
+ ```
434
+
435
+ `package_integrity_test.rb` exercises the actual publisher verification recipe with valid, tampered, missing, and malformed inputs, plus a mismatched release tag. It never runs registry publishing commands. Release outputs are ignored by Git, and temporary package installations are removed after the checks finish.
436
+
191
437
  Bug reports and pull requests are welcome on GitHub at [https://github.com/stiermid/monoholic](https://github.com/stiermid/monoholic). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](https://www.contributor-covenant.org/) code of conduct.
192
438
 
193
439
  ## License
data/_includes/menu.html CHANGED
@@ -1,21 +1,25 @@
1
- {%- assign _level = include.level | default: 2 | plus: 0 -%}
2
- {%- if _level < 2 -%}
3
- {%- assign _level = 2 -%}
4
- {%- elsif _level > 6 -%}
5
- {%- assign _level = 6 -%}
6
- {%- endif -%}
7
- {%- assign _next_level = _level | plus: 1 -%}
8
- {%- if _next_level > 6 -%}
9
- {%- assign _next_level = 6 -%}
10
- {%- endif -%}
11
1
  {%- for item in include.collection -%}
2
+ {%- comment -%}
3
+ Liquid assigns are shared across recursive includes. Recompute each entry
4
+ from the scoped include parameters before rendering its heading or children.
5
+ {%- endcomment -%}
6
+ {%- assign _level = include.level | default: 2 | plus: 0 -%}
7
+ {%- if _level < 2 -%}
8
+ {%- assign _level = 2 -%}
9
+ {%- elsif _level > 6 -%}
10
+ {%- assign _level = 6 -%}
11
+ {%- endif -%}
12
12
  {%- assign _title_stripped = item.title | strip -%}
13
13
  {%- assign _has_title = true -%}
14
- {%- if item.title == nil or _title_stripped == empty -%}
14
+ {%- if item.title == null or _title_stripped == empty -%}
15
15
  {%- assign _has_title = false -%}
16
16
  {%- endif -%}
17
+ {%- assign _next_level = _level -%}
18
+ {%- if _has_title and _level < 6 -%}
19
+ {%- assign _next_level = _level | plus: 1 -%}
20
+ {%- endif -%}
17
21
  {%- assign _title_slug = item.title | slugify -%}
18
- {%- if _title_slug == empty or _title_slug == nil -%}
22
+ {%- if _title_slug == empty or _title_slug == null -%}
19
23
  {%- assign _title_slug = 'section' -%}
20
24
  {%- endif -%}
21
25
  {%- if include.parent_id -%}
@@ -38,33 +42,40 @@
38
42
  | append: forloop.index
39
43
  -%}
40
44
  {%- endif -%}
41
- {%- if _has_title -%}
42
- <section aria-labelledby="{{ _heading_id }}">
45
+ <section
46
+ {% if _has_title %}
47
+ aria-labelledby="{{ _heading_id | escape }}"
48
+ {% endif %}
49
+ >
50
+ {%- if _has_title -%}
43
51
  {%- if _level == 3 -%}
44
- <h3 class="section-title" id="{{ _heading_id }}">
52
+ <h3 class="section-title" id="{{ _heading_id | escape }}">
45
53
  {{ item.title | escape }}
46
54
  </h3>
47
55
  {%- elsif _level == 4 -%}
48
- <h4 class="section-title" id="{{ _heading_id }}">
56
+ <h4 class="section-title" id="{{ _heading_id | escape }}">
49
57
  {{ item.title | escape }}
50
58
  </h4>
51
59
  {%- elsif _level == 5 -%}
52
- <h5 class="section-title" id="{{ _heading_id }}">
60
+ <h5 class="section-title" id="{{ _heading_id | escape }}">
53
61
  {{ item.title | escape }}
54
62
  </h5>
55
63
  {%- elsif _level >= 6 -%}
56
- <h6 class="section-title" id="{{ _heading_id }}">
64
+ <h6 class="section-title" id="{{ _heading_id | escape }}">
57
65
  {{ item.title | escape }}
58
66
  </h6>
59
67
  {%- else -%}
60
- <h2 class="section-title" id="{{ _heading_id }}">
68
+ <h2 class="section-title" id="{{ _heading_id | escape }}">
61
69
  {{ item.title | escape }}
62
70
  </h2>
63
71
  {%- endif -%}
64
- {%- else -%}
65
- <section>
66
- {%- endif -%}
67
- {%- if item.content_file -%}
72
+ {%- endif -%}
73
+ {%- if item.content_include -%}
74
+ {%- capture _menu_content -%}
75
+ {%- include {{ item.content_include }} -%}
76
+ {%- endcapture -%}
77
+ {{ _menu_content | markdownify }}
78
+ {%- elsif item.content_file -%}
68
79
  {%- assign _menu_page = site.pages
69
80
  | where: 'path', item.content_file
70
81
  | first
@@ -77,7 +88,8 @@
77
88
  {%- endif -%}
78
89
  {%- if item.post_list -%}{%- include post_list.html -%}{%- endif -%}
79
90
  {%- if item.entries -%}
80
- {%- include menu.html
91
+ {%-
92
+ include menu.html
81
93
  collection=item.entries
82
94
  level=_next_level
83
95
  parent_id=_heading_id
@@ -1,8 +1,16 @@
1
+ {%- assign _page_lang = page.lang | default: '' | strip -%}
2
+ {%- assign _site_lang = site.lang | default: '' | strip -%}
3
+ {%- assign _skip_link_label = site.theme_config.skip_link_label
4
+ | default: ''
5
+ | strip
6
+ -%}
1
7
  <!doctype html>
2
- <html lang="{{ page.lang | default: "en" }}">
8
+ <html lang="{{ _page_lang | default: _site_lang | default: 'en' | escape }}">
3
9
  {%- include head.html -%}
4
10
  <body class="{% if site.theme_config.monochrome_images != false %}images-monochrome{% endif %}">
5
- <a class="skip-link" href="#main">Skip to content</a>
11
+ <a class="skip-link" href="#main">
12
+ {{- _skip_link_label | default: 'Skip to content' | escape -}}
13
+ </a>
6
14
  <div class="wrapper">
7
15
  {%- unless page.layout == 'post' -%}
8
16
  {%- include header.html -%}
@@ -16,6 +24,14 @@
16
24
  {%- include footer.html -%}
17
25
  {%- endunless -%}
18
26
 
27
+ <script
28
+ defer
29
+ src="{{ '/assets/js/content-scroll.js' | relative_url }}"
30
+ data-table-label="{{ site.theme_config.table_scroll_label | default: 'Table' | escape }}"
31
+ data-code-label="{{ site.theme_config.code_scroll_label | default: 'Code block' | escape }}"
32
+ data-scroll-hint="{{ site.theme_config.scroll_hint | default: 'Scroll horizontally to see more.' | escape }}"
33
+ ></script>
34
+
19
35
  {% if page.custom_js %}
20
36
  {% for js_file in page.custom_js %}
21
37
  <script
data/_layouts/home.html CHANGED
@@ -1,6 +1,12 @@
1
1
  ---
2
2
  layout: default
3
3
  ---
4
- <nav aria-label="Site sections">
4
+ {{ content }}
5
+
6
+ {%- assign _site_sections_label = site.theme_config.site_sections_label
7
+ | default: ''
8
+ | strip
9
+ -%}
10
+ <nav aria-label="{{ _site_sections_label | default: 'Site sections' | escape }}">
5
11
  {%- include menu.html collection=site.data.menu.entries level=2 -%}
6
12
  </nav>
data/_sass/_base.scss CHANGED
@@ -8,6 +8,8 @@ body {
8
8
  font-family: var(--font-family);
9
9
  font-size: var(--font-size-base);
10
10
  line-height: 1.5;
11
+ // Break unusually long words and URLs only when they cannot fit.
12
+ overflow-wrap: anywhere;
11
13
  min-height: 100vh;
12
14
  margin: 0;
13
15
  padding: 0;
data/_sass/_content.scss CHANGED
@@ -4,20 +4,22 @@
4
4
  color: var(--bg-color);
5
5
  }
6
6
 
7
- div.highlighter-rouge {
8
- code {
9
- display: block;
10
- overflow-x: auto;
11
- white-space: pre-wrap;
12
- }
7
+ .page-content pre {
8
+ max-width: 100%;
9
+ overflow-x: auto;
10
+ // Keep indentation and source lines intact; scroll the block, not the page.
11
+ white-space: pre;
12
+ overflow-wrap: normal;
13
+ }
13
14
 
14
- pre.highlight {
15
- padding: var(--space-md);
16
- }
15
+ div.highlighter-rouge pre.highlight {
16
+ padding: var(--space-md);
17
17
  }
18
18
 
19
19
  table {
20
20
  width: 100%;
21
+ // Readable wrapping is the fallback when scripts are unavailable.
22
+ table-layout: fixed;
21
23
  margin-bottom: var(--space-md);
22
24
 
23
25
  &,
@@ -29,6 +31,26 @@ table {
29
31
  }
30
32
  }
31
33
 
34
+ .table-scroll {
35
+ max-width: 100%;
36
+ overflow-x: auto;
37
+ margin-bottom: var(--space-md);
38
+
39
+ table {
40
+ // Keep native table layout and semantics inside the scroll container.
41
+ table-layout: auto;
42
+ overflow-wrap: normal;
43
+ margin-bottom: 0;
44
+ }
45
+ }
46
+
47
+ .scroll-hint {
48
+ color: var(--meta-color);
49
+ font-size: var(--font-size-sm);
50
+ margin-top: calc(-1 * var(--space-sm));
51
+ margin-bottom: var(--space-md);
52
+ }
53
+
32
54
  blockquote {
33
55
  font-style: italic;
34
56
  border: thin solid var(--border-color);
@@ -7,7 +7,7 @@
7
7
 
8
8
  // Typography
9
9
  --font-family: "JetBrains Mono", monospace;
10
- --font-size-base: 16px;
10
+ --font-size-base: 1rem;
11
11
  --font-size-sm: 0.85rem;
12
12
  --font-size-section: 1.1rem;
13
13
  --font-size-site-title: 2rem;
@@ -0,0 +1,109 @@
1
+ (() => {
2
+ const settings = document.currentScript?.dataset || {};
3
+ const updates = [];
4
+ let hintIndex = 0;
5
+
6
+ const updateAll = () => updates.forEach((update) => update());
7
+ const observer =
8
+ typeof ResizeObserver === "function" ? new ResizeObserver(updateAll) : null;
9
+ const contentObserver =
10
+ typeof MutationObserver === "function"
11
+ ? new MutationObserver(updateAll)
12
+ : null;
13
+
14
+ function enhance(region, content, hintAnchor, label, labelledBy) {
15
+ // Also respect author-provided wrappers and repeated script inclusion.
16
+ if (region.hasAttribute("data-scroll-enhanced")) return;
17
+ region.setAttribute("data-scroll-enhanced", "");
18
+ const originalAttributes = new Set(region.getAttributeNames());
19
+ const hint = document.createElement("p");
20
+ do {
21
+ hint.id = `monoholic-scroll-hint-${++hintIndex}`;
22
+ } while (document.getElementById(hint.id));
23
+ hint.className = "scroll-hint";
24
+ hint.textContent =
25
+ settings.scrollHint || "Scroll horizontally to see more.";
26
+ hint.hidden = true;
27
+ hintAnchor.after(hint);
28
+
29
+ function setAutomaticAttribute(name, value) {
30
+ if (originalAttributes.has(name)) return;
31
+ if (value === null) region.removeAttribute(name);
32
+ else region.setAttribute(name, value);
33
+ }
34
+
35
+ function update() {
36
+ const overflows = region.scrollWidth > region.clientWidth + 1;
37
+ hint.hidden = !overflows;
38
+ setAutomaticAttribute("tabindex", overflows ? "0" : null);
39
+ setAutomaticAttribute("role", overflows ? "region" : null);
40
+ if (
41
+ !originalAttributes.has("aria-label") &&
42
+ !originalAttributes.has("aria-labelledby")
43
+ ) {
44
+ setAutomaticAttribute(
45
+ labelledBy ? "aria-labelledby" : "aria-label",
46
+ overflows ? labelledBy || label : null,
47
+ );
48
+ }
49
+
50
+ // Preserve descriptions supplied by the author or other scripts.
51
+ const descriptions = new Set(
52
+ (region.getAttribute("aria-describedby") || "")
53
+ .split(/\s+/)
54
+ .filter(Boolean),
55
+ );
56
+ if (overflows) descriptions.add(hint.id);
57
+ else descriptions.delete(hint.id);
58
+ if (descriptions.size) {
59
+ region.setAttribute("aria-describedby", [...descriptions].join(" "));
60
+ } else {
61
+ region.removeAttribute("aria-describedby");
62
+ }
63
+ }
64
+
65
+ updates.push(update);
66
+ update();
67
+ observer?.observe(region);
68
+ if (content !== region) observer?.observe(content);
69
+ contentObserver?.observe(content, {
70
+ childList: true,
71
+ characterData: true,
72
+ subtree: true,
73
+ });
74
+ }
75
+
76
+ document.querySelectorAll(".page-content table").forEach((table) => {
77
+ let wrapper = table.parentElement;
78
+ if (!wrapper.classList.contains("table-scroll")) {
79
+ wrapper = document.createElement("div");
80
+ wrapper.className = "table-scroll";
81
+ table.before(wrapper);
82
+ wrapper.append(table);
83
+ }
84
+ const label =
85
+ table.getAttribute("aria-label") ||
86
+ table.caption?.textContent.trim() ||
87
+ settings.tableLabel ||
88
+ "Table";
89
+ enhance(
90
+ wrapper,
91
+ table,
92
+ wrapper,
93
+ label,
94
+ table.getAttribute("aria-labelledby"),
95
+ );
96
+ });
97
+
98
+ document.querySelectorAll(".page-content pre").forEach((block) => {
99
+ const hintAnchor = block.closest("div.highlighter-rouge") || block;
100
+ enhance(block, block, hintAnchor, settings.codeLabel || "Code block");
101
+ });
102
+
103
+ window.addEventListener("resize", updateAll);
104
+ // A font swap can change overflow without resizing the container itself.
105
+ if (document.fonts) {
106
+ document.fonts.ready.then(updateAll);
107
+ document.fonts.addEventListener("loadingdone", updateAll);
108
+ }
109
+ })();
data/robots.txt ADDED
@@ -0,0 +1,8 @@
1
+ ---
2
+ layout: null
3
+ sitemap: false
4
+ ---
5
+ User-agent: *
6
+ Allow: /
7
+
8
+ Sitemap: {{ site.url }}{{ site.baseurl }}/sitemap.xml
metadata CHANGED
@@ -1,15 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: monoholic
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Agil Mammadov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-16 00:00:00.000000000 Z
11
+ date: 2026-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: erb
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '7.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '2.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
13
33
  - !ruby/object:Gem::Dependency
14
34
  name: jekyll
15
35
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +86,20 @@ dependencies:
66
86
  - - "~>"
67
87
  - !ruby/object:Gem::Version
68
88
  version: '2.8'
89
+ - !ruby/object:Gem::Dependency
90
+ name: jekyll-sitemap
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.4'
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '1.4'
69
103
  description:
70
104
  email:
71
105
  - mammadovagil@proton.me
@@ -96,6 +130,8 @@ files:
96
130
  - assets/favicon.ico
97
131
  - assets/favicon.svg
98
132
  - assets/js/.keep
133
+ - assets/js/content-scroll.js
134
+ - robots.txt
99
135
  homepage: https://github.com/stiermid/monoholic
100
136
  licenses:
101
137
  - GPL-3.0-only