datalog-theme 0.6.1 → 0.7.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 +4 -4
- data/CHANGELOG.md +50 -0
- data/CITATION.cff +2 -2
- data/README.md +3 -2
- data/_data/js_meta.json +29 -29
- data/_includes/components/author-bio.html +2 -2
- data/_includes/components/hero.html +19 -1
- data/_includes/head.html +5 -10
- data/_includes/header.html +1 -1
- data/_includes/meta/math-config.html +23 -1
- data/_includes/meta/scripts-loader.html +2 -1
- data/_includes/meta/syntax-config.html +19 -0
- data/_includes/scripts.html +1 -9
- data/_includes/search/index-data.json +156 -0
- data/_includes/search/page.html +136 -0
- data/_layouts/home.html +6 -0
- data/_plugins/math_preprocessor.rb +3 -0
- data/_plugins/notebook_converter.rb +0 -39
- data/_plugins/search_pages.rb +73 -0
- data/_sass/_base.scss +7 -1
- data/_sass/_components.scss +25 -3
- data/_sass/_font-fallbacks.scss +43 -0
- data/_sass/_header.scss +10 -0
- data/_sass/_layout.scss +17 -11
- data/_sass/_mathematical.scss +1 -1
- data/_sass/_phase1-enhancements.scss +45 -0
- data/_sass/_search.scss +1 -1
- data/_sass/_syntax-highlighting.scss +2 -2
- data/_sass/_theme.scss +14 -0
- data/_sass/_variables.scss +7 -4
- data/assets/img/hero-detail-640.webp +0 -0
- data/assets/img/hero-detail.webp +0 -0
- data/assets/img/social-card.png +0 -0
- data/assets/js/dist/academic.js +1 -0
- data/assets/js/dist/analytics-dashboard.js +1 -0
- data/assets/js/dist/core.js +1 -0
- data/assets/js/dist/loader.js +1 -0
- data/assets/js/dist/manifest.json +22 -0
- data/assets/js/dist/math.js +1 -0
- data/assets/js/dist/meta.json +371 -0
- data/assets/js/dist/notebook.js +1 -0
- data/assets/js/dist/search.js +1 -0
- data/assets/js/dist/visualizations.js +2 -0
- data/assets/js/search/filters.js +9 -2
- data/datalog-theme.gemspec +10 -2
- data/lib/datalog/cli.rb +19 -1
- data/lib/datalog/theme/version.rb +1 -1
- data/lib/datalog-theme.rb +16 -0
- metadata +34 -20
- data/_data/config/features.yml +0 -262
- data/_data/config/site.yml +0 -42
- data/_data/config/theme.yml +0 -181
- data/assets/img/20220607123041_detail.001.png +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
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 searchable = '' | split: '' %}
|
|
6
|
+
{% for post in site.posts %}
|
|
7
|
+
{% unless post.exclude_from_search %}
|
|
8
|
+
{% assign searchable = searchable | push: post %}
|
|
9
|
+
{% endunless %}
|
|
10
|
+
{% endfor %}
|
|
11
|
+
{% for page in site.pages %}
|
|
12
|
+
{% unless page.exclude_from_search or page.layout == nil or page.url contains 'search.json' %}
|
|
13
|
+
{% assign searchable = searchable | push: page %}
|
|
14
|
+
{% endunless %}
|
|
15
|
+
{% endfor %}
|
|
16
|
+
{% if site.portfolio %}
|
|
17
|
+
{% for project in site.portfolio %}
|
|
18
|
+
{% unless project.exclude_from_search %}
|
|
19
|
+
{% assign searchable = searchable | push: project %}
|
|
20
|
+
{% endunless %}
|
|
21
|
+
{% endfor %}
|
|
22
|
+
{% endif %}
|
|
23
|
+
{% if site.datasets %}
|
|
24
|
+
{% for dataset in site.datasets %}
|
|
25
|
+
{% unless dataset.exclude_from_search %}
|
|
26
|
+
{% assign searchable = searchable | push: dataset %}
|
|
27
|
+
{% endunless %}
|
|
28
|
+
{% endfor %}
|
|
29
|
+
{% endif %}
|
|
30
|
+
{% for doc in searchable %}
|
|
31
|
+
{% assign raw_tags = doc.tags | default: doc.topics %}
|
|
32
|
+
{% if raw_tags %}
|
|
33
|
+
{% assign tags = raw_tags %}
|
|
34
|
+
{% else %}
|
|
35
|
+
{% assign tags = '' | split: '' %}
|
|
36
|
+
{% endif %}
|
|
37
|
+
{% assign languages = '' | split: '' %}
|
|
38
|
+
{% if doc.languages %}
|
|
39
|
+
{% for lang in doc.languages %}
|
|
40
|
+
{% assign lang_normalized = lang | downcase %}
|
|
41
|
+
{% if known_langs contains lang_normalized %}
|
|
42
|
+
{% unless languages contains lang_normalized %}
|
|
43
|
+
{% assign languages = languages | push: lang_normalized %}
|
|
44
|
+
{% endunless %}
|
|
45
|
+
{% endif %}
|
|
46
|
+
{% endfor %}
|
|
47
|
+
{% elsif doc.language %}
|
|
48
|
+
{% assign lang_normalized = doc.language | downcase %}
|
|
49
|
+
{% if known_langs contains lang_normalized %}
|
|
50
|
+
{% assign languages = languages | push: lang_normalized %}
|
|
51
|
+
{% endif %}
|
|
52
|
+
{% elsif doc.tools %}
|
|
53
|
+
{% for tool in doc.tools %}
|
|
54
|
+
{% assign lang_normalized = tool | downcase %}
|
|
55
|
+
{% if known_langs contains lang_normalized %}
|
|
56
|
+
{% unless languages contains lang_normalized %}
|
|
57
|
+
{% assign languages = languages | push: lang_normalized %}
|
|
58
|
+
{% endunless %}
|
|
59
|
+
{% endif %}
|
|
60
|
+
{% endfor %}
|
|
61
|
+
{% elsif tags %}
|
|
62
|
+
{% for tag in tags %}
|
|
63
|
+
{% assign lang_normalized = tag | downcase %}
|
|
64
|
+
{% if known_langs contains lang_normalized %}
|
|
65
|
+
{% unless languages contains lang_normalized %}
|
|
66
|
+
{% assign languages = languages | push: lang_normalized %}
|
|
67
|
+
{% endunless %}
|
|
68
|
+
{% endif %}
|
|
69
|
+
{% endfor %}
|
|
70
|
+
{% endif %}
|
|
71
|
+
{% assign raw_content = doc.content | default: '' %}
|
|
72
|
+
{% assign content_text = raw_content | markdownify | strip_html | normalize_whitespace %}
|
|
73
|
+
{% assign summary = doc.summary | default: doc.excerpt | default: content_text | truncate: 280, '' %}
|
|
74
|
+
{% assign inline_math_segments = raw_content | split: '$' %}
|
|
75
|
+
{% assign display_math_segments = raw_content | split: '$$' %}
|
|
76
|
+
{% capture doc_type %}
|
|
77
|
+
{% 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 %}
|
|
78
|
+
{% endcapture %}
|
|
79
|
+
{% assign normalized_title = doc.title | default: doc.name | normalize_search %}
|
|
80
|
+
{% assign normalized_summary = summary | normalize_search %}
|
|
81
|
+
{% assign normalized_content = content_text | normalize_search %}
|
|
82
|
+
{% assign normalized_tags = tags | normalize_search_array %}
|
|
83
|
+
{% assign normalized_languages = languages | normalize_search_array %}
|
|
84
|
+
{% assign normalized_difficulty = doc.difficulty | default: doc.level | normalize_search | default: '' %}
|
|
85
|
+
{
|
|
86
|
+
"title": {{ doc.title | default: doc.name | jsonify }},
|
|
87
|
+
"url": {{ doc.url | relative_url | jsonify }},
|
|
88
|
+
"type": {{ doc_type | strip | jsonify }},
|
|
89
|
+
"summary": {{ summary | strip | jsonify }},
|
|
90
|
+
"content": {{ content_text | jsonify }},
|
|
91
|
+
"code": [
|
|
92
|
+
{% assign raw_blocks = raw_content | split: '```' %}
|
|
93
|
+
{% assign code_counter = 0 %}
|
|
94
|
+
{% for block in raw_blocks %}
|
|
95
|
+
{% assign block_index = forloop.index0 | modulo: 2 %}
|
|
96
|
+
{% if block_index == 1 %}
|
|
97
|
+
{% assign lines = block | split: '\n' %}
|
|
98
|
+
{% assign first_line = lines[0] | default: '' %}
|
|
99
|
+
{% assign language_hint = first_line | strip | downcase | default: 'text' %}
|
|
100
|
+
{% assign code_body = block | remove_first: first_line | strip %}
|
|
101
|
+
{% if code_counter > 0 %},{% endif %}
|
|
102
|
+
{
|
|
103
|
+
"language": {{ language_hint | jsonify }},
|
|
104
|
+
"code": {{ code_body | jsonify }}
|
|
105
|
+
}
|
|
106
|
+
{% assign code_counter = code_counter | plus: 1 %}
|
|
107
|
+
{% endif %}
|
|
108
|
+
{% endfor %}
|
|
109
|
+
],
|
|
110
|
+
"math": [
|
|
111
|
+
{% assign math_counter = 0 %}
|
|
112
|
+
{% for piece in inline_math_segments %}
|
|
113
|
+
{% assign piece_index = forloop.index0 | modulo: 2 %}
|
|
114
|
+
{% if piece_index == 1 %}
|
|
115
|
+
{% assign cleaned = piece | strip %}
|
|
116
|
+
{% if cleaned != '' %}
|
|
117
|
+
{% if math_counter > 0 %},{% endif %}
|
|
118
|
+
{{ cleaned | jsonify }}
|
|
119
|
+
{% assign math_counter = math_counter | plus: 1 %}
|
|
120
|
+
{% endif %}
|
|
121
|
+
{% endif %}
|
|
122
|
+
{% endfor %}
|
|
123
|
+
{% for piece in display_math_segments %}
|
|
124
|
+
{% assign piece_index = forloop.index0 | modulo: 2 %}
|
|
125
|
+
{% if piece_index == 1 %}
|
|
126
|
+
{% assign cleaned = piece | strip %}
|
|
127
|
+
{% if cleaned != '' %}
|
|
128
|
+
{% if math_counter > 0 %},{% endif %}
|
|
129
|
+
{{ cleaned | jsonify }}
|
|
130
|
+
{% assign math_counter = math_counter | plus: 1 %}
|
|
131
|
+
{% endif %}
|
|
132
|
+
{% endif %}
|
|
133
|
+
{% endfor %}
|
|
134
|
+
],
|
|
135
|
+
"tags": {{ tags | uniq | jsonify }},
|
|
136
|
+
"languages": {{ languages | uniq | jsonify }},
|
|
137
|
+
"difficulty": {{ doc.difficulty | default: doc.level | default: '' | jsonify }},
|
|
138
|
+
"topics": {{ doc.topics | default: tags | uniq | jsonify }},
|
|
139
|
+
"date": {% if doc.date %}{{ doc.date | date_to_xmlschema | jsonify }}{% else %}null{% endif %},
|
|
140
|
+
"reading_time": {{ doc.reading_time | default: doc.time | default: '' | jsonify }},
|
|
141
|
+
"normalized": {
|
|
142
|
+
"title": {{ normalized_title | jsonify }},
|
|
143
|
+
"summary": {{ normalized_summary | jsonify }},
|
|
144
|
+
"content": {{ normalized_content | jsonify }},
|
|
145
|
+
"tags": {{ normalized_tags | uniq | jsonify }},
|
|
146
|
+
"languages": {{ normalized_languages | uniq | jsonify }},
|
|
147
|
+
"difficulty": {{ normalized_difficulty | jsonify }}
|
|
148
|
+
}
|
|
149
|
+
{% assign search_extensions = doc.datalog_search_extensions %}
|
|
150
|
+
{% if search_extensions and search_extensions != empty %}
|
|
151
|
+
,"extensions": {{ search_extensions | jsonify }}
|
|
152
|
+
{% endif %}
|
|
153
|
+
}{% unless forloop.last %},{% endunless %}
|
|
154
|
+
{% endfor %}
|
|
155
|
+
]
|
|
156
|
+
}
|
|
@@ -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 & blog posts</option>
|
|
34
|
+
<option value="research">Research outputs</option>
|
|
35
|
+
<option value="project">Projects & 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>
|
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 %}
|
|
@@ -165,6 +165,9 @@ module MathPreprocessor
|
|
|
165
165
|
def apply(document)
|
|
166
166
|
return unless document.respond_to?(:content)
|
|
167
167
|
return unless document.respond_to?(:output_ext) && document.output_ext == ".html"
|
|
168
|
+
# A page that opts out of math rendering (`math: false` or `mathjax: false`)
|
|
169
|
+
# keeps its dollar signs and TeX-looking text verbatim.
|
|
170
|
+
return if document.respond_to?(:data) && (document.data["math"] == false || document.data["mathjax"] == false)
|
|
168
171
|
|
|
169
172
|
content = document.content
|
|
170
173
|
return unless content&.match?(/\$|\\\(|\\\[|\\begin\{/)
|
|
@@ -629,31 +629,9 @@ module Datalog
|
|
|
629
629
|
nil
|
|
630
630
|
end
|
|
631
631
|
|
|
632
|
-
def patch_jupyter_converter!
|
|
633
|
-
return unless defined?(JekyllJupyterNotebook::Converter)
|
|
634
|
-
return if @converter_fallback_applied
|
|
635
|
-
|
|
636
|
-
fallback = Module.new do
|
|
637
|
-
def convert(content)
|
|
638
|
-
super
|
|
639
|
-
rescue Errno::ENOENT, StandardError => e
|
|
640
|
-
Jekyll.logger.warn("notebook converter", "primary conversion failed: #{e.message}; using fallback renderer")
|
|
641
|
-
Datalog::NotebookRenderer.render_from_raw(content) || ""
|
|
642
|
-
end
|
|
643
|
-
end
|
|
644
|
-
|
|
645
|
-
JekyllJupyterNotebook::Converter.prepend(fallback)
|
|
646
|
-
@converter_fallback_applied = true
|
|
647
|
-
rescue StandardError => e
|
|
648
|
-
Jekyll.logger.warn("notebook converter", "failed to apply converter fallback: #{e.message}")
|
|
649
|
-
end
|
|
650
632
|
end
|
|
651
633
|
end
|
|
652
634
|
|
|
653
|
-
Jekyll::Hooks.register :site, :after_init do |_site|
|
|
654
|
-
Datalog::NotebookRenderer.patch_jupyter_converter!
|
|
655
|
-
end
|
|
656
|
-
|
|
657
635
|
module Jekyll
|
|
658
636
|
# Converts Jupyter notebooks into HTML pages and downloadable assets.
|
|
659
637
|
class NotebookConverter < Generator
|
|
@@ -671,8 +649,6 @@ module Jekyll
|
|
|
671
649
|
return
|
|
672
650
|
end
|
|
673
651
|
|
|
674
|
-
ensure_dependency
|
|
675
|
-
|
|
676
652
|
files = notebook_files
|
|
677
653
|
logger.debug("notebook converter", "located #{files.size} notebooks")
|
|
678
654
|
return if files.empty?
|
|
@@ -722,21 +698,6 @@ module Jekyll
|
|
|
722
698
|
value.sub(%r{/+$}, "")
|
|
723
699
|
end
|
|
724
700
|
|
|
725
|
-
# Notebook pages do not need the gem (see #convert_notebook). When it is
|
|
726
|
-
# present, its converter is given the same renderer as a fallback so the
|
|
727
|
-
# gem's own `.ipynb` handling and `{% jupyter_notebook %}` tag keep working
|
|
728
|
-
# on machines without a `jupyter` executable.
|
|
729
|
-
def ensure_dependency
|
|
730
|
-
return true if defined?(JekyllJupyterNotebook::Converter)
|
|
731
|
-
|
|
732
|
-
require "jekyll-jupyter-notebook"
|
|
733
|
-
Datalog::NotebookRenderer.patch_jupyter_converter!
|
|
734
|
-
true
|
|
735
|
-
rescue LoadError => e
|
|
736
|
-
logger.debug("notebook converter", "jekyll-jupyter-notebook not loaded: #{e.message}")
|
|
737
|
-
false
|
|
738
|
-
end
|
|
739
|
-
|
|
740
701
|
def notebook_files
|
|
741
702
|
glob = File.join(site.source, config["source"], "**", "*.ipynb")
|
|
742
703
|
Dir.glob(glob)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jekyll"
|
|
4
|
+
|
|
5
|
+
module Datalog
|
|
6
|
+
# Gives sites that install the theme the two pages search needs.
|
|
7
|
+
#
|
|
8
|
+
# A theme gem can ship layouts, includes, sass and assets, but not pages: a
|
|
9
|
+
# site's pages come from its own source directory. The theme therefore
|
|
10
|
+
# shipped the search interface and its JavaScript while the page that renders
|
|
11
|
+
# it and the page that builds its index stayed behind in this repository, so
|
|
12
|
+
# search was missing entirely from every site using the gem.
|
|
13
|
+
#
|
|
14
|
+
# Both are generated here from the shipped includes, for any site with
|
|
15
|
+
# `features.search` enabled that has not already defined them itself.
|
|
16
|
+
class SearchPages < Jekyll::Generator
|
|
17
|
+
safe true
|
|
18
|
+
priority :low
|
|
19
|
+
|
|
20
|
+
INDEX_URL = "/search.json"
|
|
21
|
+
PAGE_URL = "/search/"
|
|
22
|
+
|
|
23
|
+
def generate(site)
|
|
24
|
+
return unless search_enabled?(site)
|
|
25
|
+
|
|
26
|
+
add_index(site) unless defines?(site, INDEX_URL)
|
|
27
|
+
add_page(site) unless defines?(site, PAGE_URL)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def search_enabled?(site)
|
|
33
|
+
features = site.config["features"]
|
|
34
|
+
features.is_a?(Hash) && features["search"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A site that ships its own search page or index keeps it untouched.
|
|
38
|
+
def defines?(site, url)
|
|
39
|
+
site.pages.any? { |page| page.url == url }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def add_index(site)
|
|
43
|
+
page = Jekyll::PageWithoutAFile.new(site, site.source, "", "search.json")
|
|
44
|
+
page.content = "{% include search/index-data.json %}"
|
|
45
|
+
page.data.merge!(
|
|
46
|
+
"layout" => nil,
|
|
47
|
+
"permalink" => INDEX_URL,
|
|
48
|
+
"sitemap" => false
|
|
49
|
+
)
|
|
50
|
+
site.pages << page
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def add_page(site)
|
|
54
|
+
search_config = site.config["search"]
|
|
55
|
+
search_config = {} unless search_config.is_a?(Hash)
|
|
56
|
+
|
|
57
|
+
page = Jekyll::PageWithoutAFile.new(site, site.source, "search", "index.html")
|
|
58
|
+
page.content = "{% include search/page.html %}"
|
|
59
|
+
page.data.merge!(
|
|
60
|
+
"layout" => search_config["layout"] || "page",
|
|
61
|
+
"title" => search_config["title"] || "Search",
|
|
62
|
+
"permalink" => PAGE_URL,
|
|
63
|
+
"page_classes" => "search-page",
|
|
64
|
+
# Results can contain LaTeX and code, so both engines are wanted here
|
|
65
|
+
# even when the rest of the site loads them only where they appear.
|
|
66
|
+
"math" => true,
|
|
67
|
+
"syntax_highlighting" => true
|
|
68
|
+
)
|
|
69
|
+
page.data["subtitle"] = search_config["subtitle"] if search_config["subtitle"]
|
|
70
|
+
site.pages << page
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/_sass/_base.scss
CHANGED
|
@@ -48,7 +48,7 @@ a {
|
|
|
48
48
|
|
|
49
49
|
&:hover,
|
|
50
50
|
&:focus {
|
|
51
|
-
color: $color-accent-secondary;
|
|
51
|
+
color: $color-accent-secondary-text;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -100,6 +100,12 @@ pre {
|
|
|
100
100
|
z-index: $z-skiplink;
|
|
101
101
|
transition: transform $transition-base;
|
|
102
102
|
|
|
103
|
+
// body.dark-mode a outranks .skip-link and would paint the text light blue
|
|
104
|
+
// on the blue pill (2:1); keep it white in both modes (5.2:1).
|
|
105
|
+
body.dark-mode & {
|
|
106
|
+
color: white;
|
|
107
|
+
}
|
|
108
|
+
|
|
103
109
|
&:focus {
|
|
104
110
|
transform: translate(-50%, 0);
|
|
105
111
|
@include focus-ring(white);
|
data/_sass/_components.scss
CHANGED
|
@@ -109,6 +109,28 @@ body.dark-mode .btn-secondary {
|
|
|
109
109
|
border-color: $dark-accent-primary;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// `btn--ghost` is used in the markup but was never given a style, so those
|
|
113
|
+
// controls fell back to the browser's own button chrome: a light grey face
|
|
114
|
+
// that dark mode could not reach, leaving light text on it at 1.3:1. The
|
|
115
|
+
// theme tokens below follow whichever mode is active.
|
|
116
|
+
.btn--ghost {
|
|
117
|
+
background: transparent;
|
|
118
|
+
color: var(--color-text-primary);
|
|
119
|
+
border-color: var(--color-border);
|
|
120
|
+
|
|
121
|
+
&:hover {
|
|
122
|
+
background: rgba($color-accent-primary, 0.08);
|
|
123
|
+
border-color: $color-accent-primary;
|
|
124
|
+
color: $color-accent-primary;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
body.dark-mode .btn--ghost:hover {
|
|
129
|
+
background: rgba($dark-accent-primary, 0.16);
|
|
130
|
+
border-color: $dark-accent-primary;
|
|
131
|
+
color: $dark-accent-primary;
|
|
132
|
+
}
|
|
133
|
+
|
|
112
134
|
.stat {
|
|
113
135
|
display: grid;
|
|
114
136
|
gap: map.get($spacing, 1);
|
|
@@ -126,7 +148,7 @@ body.dark-mode .btn-secondary {
|
|
|
126
148
|
|
|
127
149
|
.stat__value {
|
|
128
150
|
font-size: 1.75rem;
|
|
129
|
-
color: $color-accent-secondary;
|
|
151
|
+
color: $color-accent-secondary-text;
|
|
130
152
|
}
|
|
131
153
|
|
|
132
154
|
body.dark-mode .stat {
|
|
@@ -1148,7 +1170,7 @@ body.dark-mode {
|
|
|
1148
1170
|
|
|
1149
1171
|
.notebook-output--widget {
|
|
1150
1172
|
font-style: italic;
|
|
1151
|
-
color: $color-accent-secondary;
|
|
1173
|
+
color: $color-accent-secondary-text;
|
|
1152
1174
|
}
|
|
1153
1175
|
|
|
1154
1176
|
.notebook-segment {
|
|
@@ -1569,7 +1591,7 @@ body.dark-mode {
|
|
|
1569
1591
|
&:hover,
|
|
1570
1592
|
&:focus-visible {
|
|
1571
1593
|
background: rgba($color-accent-secondary, 0.16);
|
|
1572
|
-
color: $color-accent-secondary;
|
|
1594
|
+
color: $color-accent-secondary-text;
|
|
1573
1595
|
}
|
|
1574
1596
|
}
|
|
1575
1597
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Metric-compatible local fallbacks for the IBM Plex web fonts.
|
|
2
|
+
//
|
|
3
|
+
// The Google Fonts stylesheet loads with `display=swap`, so text first paints
|
|
4
|
+
// in a local font and swaps once Plex arrives. These faces scale the local
|
|
5
|
+
// fonts to Plex's metrics (size-adjust from the average glyph width, the
|
|
6
|
+
// overrides from ascent, descent and line gap), so the swap barely moves the
|
|
7
|
+
// layout. Values derive from the fonts' metric tables (@capsizecss/metrics).
|
|
8
|
+
|
|
9
|
+
@font-face {
|
|
10
|
+
font-family: 'IBM Plex Sans Fallback';
|
|
11
|
+
src: local('Arial'), local('Liberation Sans'), local('Helvetica Neue'), local('Helvetica');
|
|
12
|
+
size-adjust: 101.17%;
|
|
13
|
+
ascent-override: 101.32%;
|
|
14
|
+
descent-override: 27.18%;
|
|
15
|
+
line-gap-override: 0%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@font-face {
|
|
19
|
+
font-family: 'IBM Plex Serif Fallback';
|
|
20
|
+
src: local('Georgia');
|
|
21
|
+
size-adjust: 106.1%;
|
|
22
|
+
ascent-override: 96.61%;
|
|
23
|
+
descent-override: 25.92%;
|
|
24
|
+
line-gap-override: 0%;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@font-face {
|
|
28
|
+
font-family: 'IBM Plex Serif Fallback Times';
|
|
29
|
+
src: local('Times New Roman'), local('Liberation Serif'), local('Times');
|
|
30
|
+
size-adjust: 116.43%;
|
|
31
|
+
ascent-override: 88.04%;
|
|
32
|
+
descent-override: 23.62%;
|
|
33
|
+
line-gap-override: 0%;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@font-face {
|
|
37
|
+
font-family: 'IBM Plex Mono Fallback';
|
|
38
|
+
src: local('Courier New'), local('Liberation Mono'), local('Courier');
|
|
39
|
+
size-adjust: 99.98%;
|
|
40
|
+
ascent-override: 102.52%;
|
|
41
|
+
descent-override: 27.5%;
|
|
42
|
+
line-gap-override: 0%;
|
|
43
|
+
}
|
data/_sass/_header.scss
CHANGED
|
@@ -226,6 +226,16 @@ body.dark-mode .header-control {
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// The menu is collapsed in the markup so the page does not reflow (and animate
|
|
230
|
+
// the reflow) when the navigation script initialises. Without JavaScript
|
|
231
|
+
// nothing could open it, so keep it visible in that case; head.html adds the
|
|
232
|
+
// `js` class before the body renders.
|
|
233
|
+
html:not(.js) .site-nav {
|
|
234
|
+
max-height: none;
|
|
235
|
+
opacity: 1;
|
|
236
|
+
pointer-events: auto;
|
|
237
|
+
}
|
|
238
|
+
|
|
229
239
|
.nav-list {
|
|
230
240
|
display: flex;
|
|
231
241
|
flex-direction: column;
|