datalog-theme 0.6.1 → 0.8.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.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -0
  3. data/CITATION.cff +2 -2
  4. data/README.md +26 -19
  5. data/_data/cdn-integrity.yml +0 -30
  6. data/_includes/analytics/dashboard.html +3 -1
  7. data/_includes/components/api-function.html +20 -1
  8. data/_includes/components/author-bio.html +2 -2
  9. data/_includes/components/enhanced-code-block.html +1 -1
  10. data/_includes/components/hero.html +19 -1
  11. data/_includes/csp-meta.html +115 -11
  12. data/_includes/footer.html +19 -20
  13. data/_includes/head.html +61 -29
  14. data/_includes/header/navigation.html +12 -15
  15. data/_includes/header.html +29 -20
  16. data/_includes/layouts/default/article.html +5 -1
  17. data/_includes/meta/math-config.html +27 -1
  18. data/_includes/meta/schema.html +5 -2
  19. data/_includes/meta/scripts-loader.html +16 -31
  20. data/_includes/post/related-posts.html +4 -7
  21. data/_includes/scripts.html +1 -9
  22. data/_includes/search/index-data.json +131 -0
  23. data/_includes/search/page.html +136 -0
  24. data/_layouts/dataset.html +1 -0
  25. data/_layouts/default.html +16 -8
  26. data/_layouts/home.html +6 -0
  27. data/_layouts/notebook.html +1 -0
  28. data/_layouts/package.html +1 -0
  29. data/_layouts/portfolio.html +1 -0
  30. data/_layouts/post.html +20 -5
  31. data/_layouts/project.html +2 -1
  32. data/_plugins/analytics_dashboard.rb +9 -3
  33. data/_plugins/config_validator.rb +12 -7
  34. data/_plugins/csp_generator.rb +18 -28
  35. data/_plugins/datalog_bibliography.rb +9 -7
  36. data/_plugins/datalog_comments.rb +8 -5
  37. data/_plugins/datalog_slides.rb +9 -8
  38. data/_plugins/i18n.rb +7 -8
  39. data/_plugins/image_optimizer.rb +17 -6
  40. data/_plugins/math_preprocessor.rb +36 -3
  41. data/_plugins/notebook_converter.rb +23 -43
  42. data/_plugins/plugin_loader.rb +3 -1
  43. data/_plugins/publications_generator.rb +8 -2
  44. data/_plugins/rouge_highlight_filter.rb +42 -0
  45. data/_plugins/search_code_blocks.rb +30 -0
  46. data/_plugins/search_normalizer.rb +15 -53
  47. data/_plugins/search_pages.rb +72 -0
  48. data/_sass/_academic-dashboard.scss +262 -0
  49. data/_sass/_base.scss +19 -1
  50. data/_sass/_components.scss +77 -1144
  51. data/_sass/_features.scss +17 -0
  52. data/_sass/_font-fallbacks.scss +43 -0
  53. data/_sass/_header.scss +10 -0
  54. data/_sass/_layout.scss +18 -12
  55. data/_sass/_mathematical.scss +1 -1
  56. data/_sass/_notebooks.scss +322 -0
  57. data/_sass/_open-science-badges.scss +56 -0
  58. data/_sass/{_phase1-enhancements.scss → _post-components.scss} +46 -1
  59. data/_sass/_search-page.scss +530 -0
  60. data/_sass/_search.scss +47 -1
  61. data/_sass/_syntax-highlighting.scss +212 -97
  62. data/_sass/_theme.scss +42 -18
  63. data/_sass/_typography.scss +14 -0
  64. data/_sass/_variables.scss +7 -4
  65. data/assets/css/main.scss +14 -0
  66. data/assets/img/hero-detail-640.webp +0 -0
  67. data/assets/img/hero-detail.webp +0 -0
  68. data/assets/img/social-card.png +0 -0
  69. data/assets/js/dist/academic.js +1 -0
  70. data/assets/js/dist/analytics-dashboard.js +1 -0
  71. data/assets/js/dist/chunks/chunk-225H5YXE.js +1 -0
  72. data/assets/js/dist/core.js +1 -0
  73. data/assets/js/dist/loader.js +1 -0
  74. data/assets/js/dist/math.js +1 -0
  75. data/assets/js/dist/notebook.js +1 -0
  76. data/assets/js/dist/search.js +1 -0
  77. data/assets/js/dist/visualizations.js +11 -0
  78. data/assets/js/loader.js +3 -1
  79. data/datalog-theme.gemspec +43 -22
  80. data/lib/datalog/cli.rb +53 -15
  81. data/lib/datalog/plugin_system/dependency_resolver.rb +0 -2
  82. data/lib/datalog/theme/package.rb +57 -0
  83. data/lib/datalog/theme/repository_checkout.rb +90 -0
  84. data/lib/datalog/theme/version.rb +1 -1
  85. data/lib/datalog/warning_filter.rb +5 -11
  86. data/lib/datalog-theme.rb +21 -0
  87. metadata +75 -146
  88. data/_data/academic.yml +0 -217
  89. data/_data/config/author.yml +0 -121
  90. data/_data/config/features.yml +0 -262
  91. data/_data/config/site.yml +0 -42
  92. data/_data/config/theme.yml +0 -181
  93. data/_data/datasets.yml +0 -28
  94. data/_data/js_meta.json +0 -371
  95. data/_data/navigation.yml +0 -145
  96. data/_data/projects.yml +0 -41
  97. data/_data/publications.yml +0 -28
  98. data/_data/social.yml +0 -73
  99. data/_data/visualizations.yml +0 -51
  100. data/_includes/components/advanced-search.html +0 -682
  101. data/_includes/components/bookmark-system.html +0 -96
  102. data/_includes/components/comments.html +0 -244
  103. data/_includes/components/content-recommendations.html +0 -228
  104. data/_includes/components/email-preferences.html +0 -200
  105. data/_includes/components/enhanced-metadata.html +0 -228
  106. data/_includes/components/language-switcher.html +0 -396
  107. data/_includes/components/navigation-enhancements.html +0 -454
  108. data/_includes/components/newsletter-signup.html +0 -178
  109. data/_includes/components/popular-posts.html +0 -233
  110. data/_includes/components/reading-progress.html +0 -133
  111. data/_includes/components/reading-time.html +0 -121
  112. data/_includes/components/series-navigation.html +0 -124
  113. data/_includes/components/social-proof.html +0 -34
  114. data/_includes/components/user-preferences.html +0 -566
  115. data/_layouts/archive.html +0 -282
  116. data/_layouts/post-sidebar.html +0 -183
  117. data/_sass/_phase3-enhancements.scss +0 -874
  118. data/_sass/_phase4-enhancements.scss +0 -1214
  119. data/_sass/_phase5-enhancements.scss +0 -414
  120. data/assets/img/20220607123041_detail.001.png +0 -0
  121. data/assets/js/academic.js +0 -262
  122. data/assets/js/analytics-dashboard.js +0 -382
  123. data/assets/js/core/dark-mode.js +0 -79
  124. data/assets/js/core/github-cards.js +0 -123
  125. data/assets/js/core/language-filter.js +0 -69
  126. data/assets/js/core/navigation.js +0 -184
  127. data/assets/js/core/scroll-progress.js +0 -45
  128. data/assets/js/core/search-hotkeys.js +0 -62
  129. data/assets/js/core/skip-links.js +0 -62
  130. data/assets/js/main.js +0 -23
  131. data/assets/js/math.js +0 -818
  132. data/assets/js/notebook.js +0 -158
  133. data/assets/js/search/analytics.js +0 -91
  134. data/assets/js/search/app.js +0 -271
  135. data/assets/js/search/autocomplete.js +0 -120
  136. data/assets/js/search/engine.js +0 -260
  137. data/assets/js/search/filters.js +0 -38
  138. data/assets/js/search/render.js +0 -217
  139. data/assets/js/search/utils.js +0 -99
  140. data/assets/js/search.js +0 -354
  141. data/assets/js/visualizations.js +0 -816
  142. data/assets/publications/datalog-publications.bib +0 -8
  143. data/assets/publications/datalog-publications.ris +0 -9
  144. data/assets/publications/publications.bib +0 -30
  145. data/assets/templates/diogo-ribeiro-cv.md +0 -31
  146. data/assets/templates/diogo-ribeiro-cv.tex +0 -32
  147. data/lib/datalog/theme/theme.rb +0 -18
@@ -2,25 +2,18 @@
2
2
  ---
3
3
  required:
4
4
  - location
5
- optional:
6
- - syntax_config
7
5
  ---
8
- Outputs Prism assets in the head and shared data bootstrapping scripts in the body.
9
- Pass location="head" or location="body" to control which block renders.
6
+ Outputs the page data scripts and the feature loader at the end of the body
7
+ (location="body"). location="head" used to render Prism's scripts. Code is now
8
+ highlighted by Rouge when the site builds, so the head renders nothing.
10
9
  {%- endcomment -%}
11
- {%- assign syntax = include.syntax_config | default: site.theme_options.syntax_highlighting | default: {} -%}
12
- {%- assign prism_cdn = syntax.cdn | default: 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0' -%}
13
- {%- assign default_components = 'python|r|sql|julia|javascript' | split: '|' -%}
14
- {%- assign prism_components = syntax.components | default: default_components -%}
15
- {%- if include.location == 'head' -%}
16
- <script defer src="{{ (prism_cdn | append: '/prism.min.js') | add_sri }}"></script>
17
- {%- for component in prism_components -%}
18
- {%- unless component == 'core' -%}
19
- <script defer src="{{ (prism_cdn | append: '/components/prism-' | append: component | append: '.min.js') | add_sri }}"></script>
20
- {%- endunless -%}
21
- {%- endfor -%}
22
- <script defer src="{{ (prism_cdn | append: '/plugins/normalize-whitespace/prism-normalize-whitespace.min.js') | add_sri }}"></script>
23
- {%- elsif include.location == 'body' -%}
10
+ {%- if include.location == 'body' -%}
11
+ {%- comment -%}
12
+ Only academic.js reads the academic and publication data, and the loader
13
+ runs it only on pages with citation metrics, tables or charts. Inlined on
14
+ every page, it was a quarter of the home page's HTML.
15
+ {%- endcomment -%}
16
+ {%- if content contains 'data-citation-metric' or content contains 'data-citation-table' or content contains 'data-citation-chart' -%}
24
17
  {%- assign publications = site.data.publications | default: {} -%}
25
18
  <script nonce="{{ page.csp_nonce }}">
26
19
  window.DatalogAcademic = {{ site.data.academic | jsonify }};
@@ -31,22 +24,14 @@ Pass location="head" or location="body" to control which block renders.
31
24
  "yearly_totals": {{ publications.yearly_totals | jsonify }}
32
25
  };
33
26
  </script>
27
+ {%- endif -%}
28
+ {%- comment -%} Not include_cached: scripts.html reads the page's math and notebook settings and CSP nonce. {%- endcomment -%}
34
29
  {% include scripts.html %}
30
+ {%- comment -%}
31
+ core/github-cards.js reads the integration settings. The theme options and
32
+ content taxonomy were inlined here as well, and no script read them.
33
+ {%- endcomment -%}
35
34
  <script nonce="{{ page.csp_nonce }}">
36
- window.DatalogTheme = {{ site.theme_options | jsonify }};
37
35
  window.DatalogIntegrations = {{ site.integrations | jsonify }};
38
- window.DatalogContent = {{ site.content | jsonify }};
39
- </script>
40
- <script nonce="{{ page.csp_nonce }}">
41
- document.addEventListener('DOMContentLoaded', function () {
42
- if (window.Prism && window.Prism.plugins && window.Prism.plugins.NormalizeWhitespace) {
43
- Prism.plugins.NormalizeWhitespace.setDefaults({
44
- 'remove-trailing': true,
45
- 'remove-indent': true,
46
- 'left-trim': true,
47
- 'right-trim': true
48
- });
49
- }
50
- });
51
36
  </script>
52
37
  {%- endif -%}
@@ -10,17 +10,14 @@ Builds a list of related posts based on overlapping tags or categories.
10
10
  Falls back to a friendly message when no related content is available.
11
11
  {%- endcomment -%}
12
12
  {%- assign related_limit = include.limit | default: 3 -%}
13
- {%- assign source_posts = site.collections.posts.docs | default: site.posts | default: '' | split: '' -%}
14
- {%- assign layout_filtered = source_posts | where: 'layout', 'post' -%}
15
- {%- assign collection_filtered = source_posts | where: 'collection', 'posts' -%}
16
- {%- assign candidate_posts = layout_filtered | concat: collection_filtered -%}
17
- {%- assign sorted_candidates = candidate_posts | sort: 'date' | reverse -%}
13
+ {%- comment -%} site.posts is already newest first. {%- endcomment -%}
18
14
  {%- assign rendered_count = 0 -%}
19
15
  <section class="post-related" aria-labelledby="post-related-heading">
20
16
  <h2 id="post-related-heading">Related posts</h2>
21
17
  <ul class="post-related-list">
22
- {%- for candidate in sorted_candidates -%}
23
- {%- if candidate.url != include.page.url and rendered_count < related_limit -%}
18
+ {%- for candidate in site.posts -%}
19
+ {%- if rendered_count >= related_limit -%}{%- break -%}{%- endif -%}
20
+ {%- if candidate.url != include.page.url -%}
24
21
  {%- assign overlap = 0 -%}
25
22
  {%- if include.page.tags and candidate.tags -%}
26
23
  {%- for tag in include.page.tags -%}
@@ -1,12 +1,4 @@
1
- {% assign math_settings = site.theme_options.math %}
2
- {% assign math_engine = page.math_engine | default: math_settings.engine | default: 'mathjax' %}
3
- {% assign math_enabled = page.mathjax %}
4
- {% if math_enabled == nil %}
5
- {% assign math_enabled = page.math %}
6
- {% endif %}
7
- {% if math_enabled == nil %}
8
- {% assign math_enabled = math_settings.render_on_load | default: site.features.mathjax %}
9
- {% endif %}
1
+ {% include meta/math-config.html page=page %}
10
2
  {% assign js_manifest = site.data.js_manifest %}
11
3
 
12
4
  {% if site.features.search %}
@@ -0,0 +1,131 @@
1
+ {
2
+ "generated_at": "{{ 'now' | date_to_xmlschema }}",
3
+ "documents": [
4
+ {% assign known_langs = 'python,r,sql,julia,javascript,scala,go,java,typescript,html,css' | split: ',' %}
5
+ {% assign no_code = '' | split: '' %}
6
+ {% assign searchable = '' | split: '' %}
7
+ {% for post in site.posts %}
8
+ {% unless post.exclude_from_search %}
9
+ {% assign searchable = searchable | push: post %}
10
+ {% endunless %}
11
+ {% endfor %}
12
+ {% for page in site.pages %}
13
+ {% unless page.exclude_from_search or page.layout == nil or page.url contains 'search.json' %}
14
+ {% assign searchable = searchable | push: page %}
15
+ {% endunless %}
16
+ {% endfor %}
17
+ {% if site.portfolio %}
18
+ {% for project in site.portfolio %}
19
+ {% unless project.exclude_from_search %}
20
+ {% assign searchable = searchable | push: project %}
21
+ {% endunless %}
22
+ {% endfor %}
23
+ {% endif %}
24
+ {% if site.datasets %}
25
+ {% for dataset in site.datasets %}
26
+ {% unless dataset.exclude_from_search %}
27
+ {% assign searchable = searchable | push: dataset %}
28
+ {% endunless %}
29
+ {% endfor %}
30
+ {% endif %}
31
+ {% for doc in searchable %}
32
+ {% assign raw_tags = doc.tags | default: doc.topics %}
33
+ {% if raw_tags %}
34
+ {% assign tags = raw_tags %}
35
+ {% else %}
36
+ {% assign tags = '' | split: '' %}
37
+ {% endif %}
38
+ {% assign languages = '' | split: '' %}
39
+ {% if doc.languages %}
40
+ {% for lang in doc.languages %}
41
+ {% assign lang_normalized = lang | downcase %}
42
+ {% if known_langs contains lang_normalized %}
43
+ {% unless languages contains lang_normalized %}
44
+ {% assign languages = languages | push: lang_normalized %}
45
+ {% endunless %}
46
+ {% endif %}
47
+ {% endfor %}
48
+ {% elsif doc.language %}
49
+ {% assign lang_normalized = doc.language | downcase %}
50
+ {% if known_langs contains lang_normalized %}
51
+ {% assign languages = languages | push: lang_normalized %}
52
+ {% endif %}
53
+ {% elsif doc.tools %}
54
+ {% for tool in doc.tools %}
55
+ {% assign lang_normalized = tool | downcase %}
56
+ {% if known_langs contains lang_normalized %}
57
+ {% unless languages contains lang_normalized %}
58
+ {% assign languages = languages | push: lang_normalized %}
59
+ {% endunless %}
60
+ {% endif %}
61
+ {% endfor %}
62
+ {% elsif tags %}
63
+ {% for tag in tags %}
64
+ {% assign lang_normalized = tag | downcase %}
65
+ {% if known_langs contains lang_normalized %}
66
+ {% unless languages contains lang_normalized %}
67
+ {% assign languages = languages | push: lang_normalized %}
68
+ {% endunless %}
69
+ {% endif %}
70
+ {% endfor %}
71
+ {% endif %}
72
+ {% assign raw_content = doc.content | default: '' %}
73
+ {% assign content_text = raw_content | markdownify | strip_html | normalize_whitespace %}
74
+ {% assign summary = doc.summary | default: doc.excerpt | default: content_text | truncate: 280, '' %}
75
+ {% assign inline_math_segments = raw_content | split: '$' %}
76
+ {% assign display_math_segments = raw_content | split: '$$' %}
77
+ {% capture doc_type %}
78
+ {% if doc.collection == 'posts' %}post{% elsif doc.collection == 'portfolio' %}project{% elsif doc.collection == 'datasets' %}dataset{% elsif doc.layout == 'research' %}research{% elsif doc.layout == 'project' %}project{% elsif doc.layout == 'post' %}post{% else %}{{ doc.collection | default: doc.layout | default: 'page' }}{% endif %}
79
+ {% endcapture %}
80
+ {%- comment -%}
81
+ Each text field is written once. The search engine normalizes it in the browser with the
82
+ function it applies to queries; a precomputed normalized copy doubled the size of the file.
83
+ The code blocks come from the page's source, collected before rendering by
84
+ _plugins/search_code_blocks.rb: by the time this renders, a post's content is already HTML.
85
+ {%- endcomment %}
86
+ {
87
+ "title": {{ doc.title | default: doc.name | jsonify }},
88
+ "url": {{ doc.url | relative_url | jsonify }},
89
+ "type": {{ doc_type | strip | jsonify }},
90
+ "summary": {{ summary | strip | jsonify }},
91
+ "content": {{ content_text | jsonify }},
92
+ "code": {{ doc.search_code | default: no_code | jsonify }},
93
+ "math": [
94
+ {% assign math_counter = 0 %}
95
+ {% for piece in inline_math_segments %}
96
+ {% assign piece_index = forloop.index0 | modulo: 2 %}
97
+ {% if piece_index == 1 %}
98
+ {% assign cleaned = piece | strip %}
99
+ {% if cleaned != '' %}
100
+ {% if math_counter > 0 %},{% endif %}
101
+ {{ cleaned | jsonify }}
102
+ {% assign math_counter = math_counter | plus: 1 %}
103
+ {% endif %}
104
+ {% endif %}
105
+ {% endfor %}
106
+ {% for piece in display_math_segments %}
107
+ {% assign piece_index = forloop.index0 | modulo: 2 %}
108
+ {% if piece_index == 1 %}
109
+ {% assign cleaned = piece | strip %}
110
+ {% if cleaned != '' %}
111
+ {% if math_counter > 0 %},{% endif %}
112
+ {{ cleaned | jsonify }}
113
+ {% assign math_counter = math_counter | plus: 1 %}
114
+ {% endif %}
115
+ {% endif %}
116
+ {% endfor %}
117
+ ],
118
+ "tags": {{ tags | uniq | jsonify }},
119
+ "languages": {{ languages | uniq | jsonify }},
120
+ "difficulty": {{ doc.difficulty | default: doc.level | default: '' | jsonify }},
121
+ "topics": {{ doc.topics | default: tags | uniq | jsonify }},
122
+ "date": {% if doc.date %}{{ doc.date | date_to_xmlschema | jsonify }}{% else %}null{% endif %},
123
+ "reading_time": {{ doc.reading_time | default: doc.time | default: '' | jsonify }}
124
+ {% assign search_extensions = doc.datalog_search_extensions %}
125
+ {% if search_extensions and search_extensions != empty %}
126
+ ,"extensions": {{ search_extensions | jsonify }}
127
+ {% endif %}
128
+ }{% unless forloop.last %},{% endunless %}
129
+ {% endfor %}
130
+ ]
131
+ }
@@ -0,0 +1,136 @@
1
+ <div class="search-app" data-search-app data-search-index="{{ '/search.json' | relative_url }}">
2
+ <div class="search-app__panel">
3
+ <form class="search-app__form" data-search-form novalidate role="search" aria-label="Technical content search">
4
+ <div class="search-app__query">
5
+ <label class="search-app__label" for="advanced-search-input">Query</label>
6
+ <div class="search-app__input-group">
7
+ <input
8
+ id="advanced-search-input"
9
+ class="search-app__input"
10
+ type="search"
11
+ name="q"
12
+ placeholder="Search code, LaTeX, datasets, and more"
13
+ autocomplete="off"
14
+ spellcheck="false"
15
+ data-search-input
16
+ aria-describedby="search-shortcuts"
17
+ aria-activedescendant=""
18
+ />
19
+ <button type="submit" class="search-app__submit">Search</button>
20
+ </div>
21
+ <p id="search-shortcuts" class="search-app__shortcuts" data-search-shortcuts>
22
+ Keyboard shortcuts: <kbd>/</kbd> or <kbd>Ctrl</kbd> + <kbd>K</kbd> to focus, <kbd>Enter</kbd> to run, <kbd>Esc</kbd> to clear.
23
+ </p>
24
+ <div class="search-app__autocomplete" data-search-autocomplete role="listbox" aria-label="Search suggestions"></div>
25
+ </div>
26
+ <fieldset class="search-app__filters" data-search-filters>
27
+ <legend>Advanced filters</legend>
28
+ <div class="search-app__filters-grid">
29
+ <label class="search-filter">
30
+ <span class="search-filter__label">Content type</span>
31
+ <select data-filter-type>
32
+ <option value="">All content</option>
33
+ <option value="post">Tutorials &amp; blog posts</option>
34
+ <option value="research">Research outputs</option>
35
+ <option value="project">Projects &amp; portfolio</option>
36
+ <option value="dataset">Datasets</option>
37
+ <option value="page">Pages</option>
38
+ </select>
39
+ </label>
40
+ <label class="search-filter">
41
+ <span class="search-filter__label">Programming language</span>
42
+ <select data-filter-language>
43
+ <option value="">Any language</option>
44
+ <option value="python">Python</option>
45
+ <option value="r">R</option>
46
+ <option value="sql">SQL</option>
47
+ <option value="julia">Julia</option>
48
+ <option value="javascript">JavaScript</option>
49
+ </select>
50
+ </label>
51
+ <label class="search-filter">
52
+ <span class="search-filter__label">Difficulty</span>
53
+ <select data-filter-difficulty>
54
+ <option value="">All levels</option>
55
+ <option value="beginner">Beginner</option>
56
+ <option value="intermediate">Intermediate</option>
57
+ <option value="advanced">Advanced</option>
58
+ </select>
59
+ </label>
60
+ <div class="search-filter">
61
+ <span class="search-filter__label">Topics</span>
62
+ <div class="search-filter__tags" data-filter-tags role="group" aria-label="Filter by topic tags"></div>
63
+ </div>
64
+ </div>
65
+ </fieldset>
66
+ </form>
67
+ <aside class="search-app__analytics" data-search-analytics aria-live="polite">
68
+ <h2 class="search-app__analytics-title">Popular technical queries</h2>
69
+ <p class="search-app__analytics-empty">Search activity will appear here once queries are made.</p>
70
+ <ol class="search-app__analytics-list"></ol>
71
+ </aside>
72
+ </div>
73
+ <section class="search-app__results" aria-live="polite" data-search-region>
74
+ <header class="search-app__results-header">
75
+ <h2>Results</h2>
76
+ <p class="search-app__results-meta" data-search-meta>Enter a query to begin.</p>
77
+ <div class="search-app__loading" data-search-loading hidden>
78
+ <span class="search-app__spinner" aria-hidden="true"></span>
79
+ <span class="search-app__loading-text">Searching…</span>
80
+ </div>
81
+ <div class="visually-hidden" data-search-announcements>
82
+ <p aria-live="polite" data-search-live="count" hidden></p>
83
+ <p aria-live="polite" data-search-live="selection" hidden></p>
84
+ <p aria-live="polite" data-search-live="status" hidden></p>
85
+ </div>
86
+ </header>
87
+ <ol class="search-app__results-list" data-search-results role="listbox" aria-label="Search results"></ol>
88
+ <template id="search-result-template">
89
+ <li class="search-result" role="option" data-result-item>
90
+ <article class="search-result__card">
91
+ <header class="search-result__header">
92
+ <p class="search-result__meta">
93
+ <span class="search-result__type" data-result-type></span>
94
+ <span class="search-result__difficulty" data-result-difficulty></span>
95
+ <span class="search-result__language" data-result-languages></span>
96
+ </p>
97
+ <h3 class="search-result__title"><a data-result-link></a></h3>
98
+ <p class="search-result__summary" data-result-summary></p>
99
+ </header>
100
+ <div class="search-result__body">
101
+ <pre class="search-result__code language-none line-numbers" data-result-code><code></code></pre>
102
+ <div class="search-result__math" data-result-math hidden>
103
+ <div class="search-result__math-preview" data-result-math-preview aria-hidden="true"></div>
104
+ <pre class="search-result__math-source"><code data-result-math-code></code></pre>
105
+ <button type="button" class="search-result__math-copy" data-result-math-copy>
106
+ <span aria-hidden="true">⧉</span>
107
+ <span class="visually-hidden">Copy LaTeX</span>
108
+ </button>
109
+ </div>
110
+ <p class="search-result__excerpt" data-result-excerpt></p>
111
+ </div>
112
+ <footer class="search-result__footer">
113
+ <ul class="search-result__tags" data-result-tags></ul>
114
+ <time class="search-result__date" data-result-date></time>
115
+ </footer>
116
+ </article>
117
+ </li>
118
+ </template>
119
+ <div class="search-app__no-results" data-search-empty hidden>
120
+ <div class="search-app__no-results-illustration" aria-hidden="true">
121
+ <svg viewBox="0 0 120 80" role="presentation" focusable="false">
122
+ <g fill="none" stroke="currentColor" stroke-width="2">
123
+ <circle cx="40" cy="34" r="18" opacity="0.35" />
124
+ <circle cx="72" cy="46" r="14" opacity="0.2" />
125
+ <path d="M28 58h64" stroke-linecap="round" opacity="0.45" />
126
+ <path d="M48 33l8 9 16-18" stroke-linecap="round" stroke-linejoin="round" />
127
+ </g>
128
+ </svg>
129
+ </div>
130
+ <p>No matching entries were found. Try refining your filters or adjusting the query.</p>
131
+ </div>
132
+ </section>
133
+ </div>
134
+ <noscript>
135
+ <p class="search-app__noscript">JavaScript is required for the interactive search experience. Please enable JavaScript to explore technical content.</p>
136
+ </noscript>
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  layout: default
3
+ show_title: false # this layout renders the page's <h1>
3
4
  ---
4
5
  {% assign localization_config = site.theme_options.localization %}
5
6
  {% assign date_format = page.date_format | default: localization_config.date_format | default: 'long' %}
@@ -8,25 +8,33 @@
8
8
  <head>
9
9
  {% include head.html %}
10
10
  <meta name="twitter:creator" content="{{ site.author.twitter | default: site.social.twitter }}" />
11
- {% if page.tags and page.tags.size > 0 %}
12
- <meta name="keywords" content="{% include helpers/array-to-sentence.html items=page.tags conjunction='and' %}" />
13
- {% endif %}
14
- <meta name="format-detection" content="telephone=no" />
15
- <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
16
11
  {% include meta/schema.html page=page %}
17
- {% include meta/scripts-loader.html location='head' %}
18
12
  </head>
19
13
  {% capture body_classes %}
20
14
  site-body{% if page.layout %} layout-{{ page.layout | replace: ' ', '-' }}{% endif %}{% if page.page_class %} page-{{ page.page_class | replace: ' ', '-' | downcase }}{% endif %}{% if page.classes %} {{ page.classes | join: ' ' }}{% endif %}{% if site.features.dark_mode_toggle %} has-dark-toggle{% endif %}{% if html_dir == 'rtl' %} rtl{% endif %}
21
15
  {% endcapture %}
22
16
  <body class="{{ body_classes | strip }}" data-theme="{{ page.theme | default: 'system' }}">
23
- {% include skip-link.html %}
17
+ {%- comment -%}
18
+ Applies the saved or system color scheme before anything is drawn. core/dark-mode.js
19
+ applies the same rule, but only once the script bundles have loaded, so readers who
20
+ chose dark saw the light theme first; keep the two in step.
21
+ {%- endcomment %}
22
+ <script nonce="{{ page.csp_nonce }}">
23
+ try {
24
+ var mode = localStorage.getItem("datalog-color-mode");
25
+ if (mode === "dark" || (mode !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
26
+ document.body.classList.add("dark-mode");
27
+ }
28
+ } catch (error) {}
29
+ </script>
30
+ {%- comment -%} The skip link and the footer take nothing from the page, so each is rendered once per build (the skip link once per language). {%- endcomment -%}
31
+ {% include_cached skip-link.html lang=page.lang %}
24
32
  {% include header.html %}
25
33
  <main id="main-content" class="site-main container" role="main" tabindex="-1" aria-describedby="{{ math_status_id }}">
26
34
  {% include components/math-fallback.html expressions=page.math_expressions %}
27
35
  {% include layouts/default/article.html page=page content=content date_format=date_format %}
28
36
  </main>
29
- {% include footer.html %}
37
+ {% include_cached footer.html %}
30
38
  <div class="visually-hidden" aria-live="polite" id="{{ math_status_id }}">{% t 'global.math_loading' %}</div>
31
39
  {% include meta/scripts-loader.html location='body' %}
32
40
  </body>
data/_layouts/home.html CHANGED
@@ -31,6 +31,11 @@ layout: default
31
31
  </div>
32
32
  </div>
33
33
  </section>
34
+ {% comment %}
35
+ The portfolio collection is optional: a site that has not declared it gets
36
+ nil here, and sorting nil raises rather than rendering an empty section.
37
+ {% endcomment %}
38
+ {% if site.portfolio and site.portfolio.size > 0 %}
34
39
  <section class="section section-portfolio">
35
40
  <div class="container">
36
41
  <h2 class="section-title">{% t 'home.featured_projects' %}</h2>
@@ -46,3 +51,4 @@ layout: default
46
51
  </div>
47
52
  </div>
48
53
  </section>
54
+ {% endif %}
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  layout: default
3
+ show_title: false # this layout renders the page's <h1>
3
4
  ---
4
5
 
5
6
  {%- comment -%}
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  layout: default
3
+ show_title: false # this layout renders the page's <h1>
3
4
  ---
4
5
 
5
6
  <div class="package-docs">
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  layout: default
3
+ show_title: false # this layout renders the page's <h1>
3
4
  ---
4
5
  <article class="portfolio-item">
5
6
  <header class="portfolio-header">
data/_layouts/post.html CHANGED
@@ -272,7 +272,12 @@ schema_type: TechnicalArticle
272
272
  const metaBar = document.createElement('div');
273
273
  metaBar.className = 'code-block-meta';
274
274
 
275
- const lang = codeBlock.getAttribute('class');
275
+ // Rouge puts the language on the block's wrapper rather than on the code.
276
+ // With line numbers on, the code is a table whose first column holds the
277
+ // numbers, and the copy leaves them out.
278
+ const langHolder = codeBlock.matches('[class*="language-"]') ? codeBlock : codeBlock.closest('.highlighter-rouge');
279
+ const lang = langHolder && langHolder.getAttribute('class');
280
+ const source = codeBlock.querySelector('.rouge-code') || codeBlock;
276
281
  if (lang) {
277
282
  const m = lang.match(/language-([a-z0-9+#]+)/i);
278
283
  if (m) {
@@ -291,7 +296,7 @@ schema_type: TechnicalArticle
291
296
 
292
297
  if (navigator.clipboard && navigator.clipboard.writeText) {
293
298
  button.addEventListener('click', function () {
294
- navigator.clipboard.writeText(codeBlock.textContent).then(function () {
299
+ navigator.clipboard.writeText(source.textContent).then(function () {
295
300
  button.textContent = 'Copied!';
296
301
  button.classList.add('is-copied');
297
302
  setTimeout(function () {
@@ -311,9 +316,19 @@ schema_type: TechnicalArticle
311
316
  wrapper.insertBefore(metaBar, pre);
312
317
  });
313
318
 
314
- // Make equation references focus their targets for accessibility.
315
- if (window.MathJax && window.MathJax.startup) {
316
- window.MathJax.startup.promise.then(function () {
319
+ // Make equation references focus their targets for accessibility. This runs
320
+ // on DOMContentLoaded, before MathJax has loaded, when window.MathJax is
321
+ // still the configuration object and has no startup.promise; reading it
322
+ // threw. The head's MathJax configuration announces typeset math instead.
323
+ const onMathReady = function (callback) {
324
+ if (document.body.classList.contains('math-ready') && document.querySelector('mjx-container')) {
325
+ callback();
326
+ } else {
327
+ document.addEventListener('datalog:math-ready', callback, { once: true });
328
+ }
329
+ };
330
+ if (window.MathJax) {
331
+ onMathReady(function () {
317
332
  const equations = document.querySelectorAll('.post-content mjx-container[display="true"]');
318
333
  let eqIndex = 1;
319
334
  equations.forEach(function (container) {
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  layout: default
3
+ show_title: false # this layout renders the page's <h1>
3
4
  ---
4
5
  {% assign localization_config = site.theme_options.localization %}
5
6
  {% assign date_format = localization_config.date_format | default: 'long' %}
@@ -30,7 +31,7 @@ layout: default
30
31
  {% if page.github %}
31
32
  <section class="project-case__github" aria-labelledby="github-heading">
32
33
  <h2 id="github-heading">{% t 'project.header.github_repository' %}</h2>
33
- <div class="project-case__github-card" data-github-repo="{{ page.github.repo }}" data-github-owner="{{ page.github.owner }}">
34
+ <div class="project-case__github-card" data-github-repo="{{ page.github.repo }}" data-github-owner="{{ page.github.owner }}" data-github-unavailable="{% t 'project.github.unavailable' %}">
34
35
  <div class="project-case__github-meta">
35
36
  <p class="project-case__github-name">
36
37
  <a href="https://github.com/{{ page.github.owner }}/{{ page.github.repo }}" itemprop="url">{{ page.github.owner }}/{{ page.github.repo }}</a>
@@ -31,7 +31,10 @@ module Datalog
31
31
 
32
32
  data = query_analytics(site)
33
33
  data["fetched_at"] = Time.now.utc.iso8601
34
- write_cache(cache_path, data)
34
+ # Only a successful report is cached. A missing-configuration or error
35
+ # payload used to be kept for CACHE_TTL as well, so a site that had just
36
+ # set GA4_PROPERTY_ID went on reporting the old problem for a day.
37
+ write_cache(cache_path, data) if data["status"] == "ok"
35
38
  data
36
39
  rescue StandardError => e
37
40
  Jekyll.logger.warn("Analytics", "Falling back to cached analytics data: #{e.message}")
@@ -43,7 +46,7 @@ module Datalog
43
46
  end
44
47
 
45
48
  def fresh?(payload)
46
- return false unless payload.is_a?(Hash)
49
+ return false unless payload.is_a?(Hash) && payload["status"] == "ok"
47
50
 
48
51
  fetched_at = payload["fetched_at"]
49
52
  return false if fetched_at.to_s.empty?
@@ -95,7 +98,10 @@ module Datalog
95
98
  results["status"] = "ok"
96
99
  results
97
100
  rescue LoadError => e
98
- fallback_payload("missing_dependency", "Install googleauth to enable GA4 integration: #{e.message}")
101
+ # The theme does not depend on googleauth: only a site with a GA4 property
102
+ # configured needs it, and adds it to its own Gemfile.
103
+ fallback_payload("missing_dependency",
104
+ "Add gem \"googleauth\" to the site's Gemfile to enable the GA4 integration: #{e.message}")
99
105
  rescue StandardError => e
100
106
  fallback_payload("error", e.message)
101
107
  end
@@ -12,8 +12,9 @@ module Datalog
12
12
  SCHEMA = {
13
13
  title: { type: :string, required: true },
14
14
  url: { type: :string, required: true, format: :url },
15
+ # `author: Jane Doe` is a common Jekyll setting; the hash adds an email and profile links.
15
16
  author: {
16
- type: :hash,
17
+ type: %i[string hash],
17
18
  required: true,
18
19
  schema: {
19
20
  name: { type: :string, required: true },
@@ -68,12 +69,6 @@ module Datalog
68
69
  engine: { type: :string, enum: %w[mathjax katex] },
69
70
  enabled: { type: :boolean }
70
71
  }
71
- },
72
- syntax_highlighting: {
73
- type: :hash,
74
- schema: {
75
- cdn: { type: :string }
76
- }
77
72
  }
78
73
  }
79
74
  }
@@ -84,6 +79,10 @@ module Datalog
84
79
  replacement: "theme_options.math.engine",
85
80
  message: "'math_engine' has moved under theme_options.math.engine.",
86
81
  auto_migrate: true
82
+ },
83
+ "theme_options.syntax_highlighting" => {
84
+ message: "Code is highlighted by Rouge when the site builds and the theme no longer loads Prism, " \
85
+ "so these settings have no effect. Remove them."
87
86
  }
88
87
  }.freeze
89
88
 
@@ -204,6 +203,8 @@ module Datalog
204
203
  end
205
204
 
206
205
  def type_valid?(value, expected_type)
206
+ return expected_type.any? { |type| type_valid?(value, type) } if expected_type.is_a?(Array)
207
+
207
208
  case expected_type
208
209
  when :string
209
210
  value.is_a?(String)
@@ -222,6 +223,8 @@ module Datalog
222
223
 
223
224
  def url_valid?(value)
224
225
  return false unless value.is_a?(String)
226
+ # `url: ""` is what `jekyll new` writes, and what a site built locally keeps.
227
+ return true if value.strip.empty?
225
228
 
226
229
  uri = URI.parse(value)
227
230
  uri.is_a?(URI::HTTP) && !uri.host.nil?
@@ -311,6 +314,8 @@ module Datalog
311
314
  end
312
315
 
313
316
  def human_type(type)
317
+ return type.map { |entry| human_type(entry) }.join(" or ") if type.is_a?(Array)
318
+
314
319
  case type
315
320
  when :string then "a String"
316
321
  when :integer then "an Integer"