hematite 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a71c298f5a705d7357954e7c3a07f84cadf351135ecc275bd594fe340bd87899
4
- data.tar.gz: 1469e07477ec5f0ae1827902f654137c1d147d55e1e414243d5d8603f6496e97
3
+ metadata.gz: 7c31b77be5a329f70985b04f78078a87cf74d79c1f462018d8eed576e18efadc
4
+ data.tar.gz: 643ca6f28aaf67740b9e71b9de4a0f21bbe08f0f139b95e7b67819b28f9b1648
5
5
  SHA512:
6
- metadata.gz: '09ee339a6bbfb33e00a96dd84c1dffeca7584853ca45fd74832b0dbd244ca1833dd27c4c955a6282ee85e87f14b8d2e66762787b00323a6c65e40d527daadee2'
7
- data.tar.gz: 8c9a200eeadec2ca02366086fd7933b0e8fc7601118c9a209cfec70160ceb73b35d76cf6a0835ea04e0f55e56c8731eef2ad1b35d48092c51a540276d886d9f4
6
+ metadata.gz: 3200f91d6ddd479b360c08b74862ff2a1260d45a92d3e84aa5c9686556cea7303835129c10329bd1b84d0cf965225eeb0808771f4d8edb338870feeb2d9c52e2
7
+ data.tar.gz: 79a331beca39bdfb4c1f8fb9447a3f04f9f36cb7f17856103b24353c875bc2ca36a77f866613d03d11daa3fff95a45de727d4a7a977eb022f5f4df31f09590d7
@@ -0,0 +1,7 @@
1
+ <!--
2
+ Include all external libraries (that are bundled with Hematite) requested by the
3
+ page
4
+ -->
5
+
6
+ {% include mermaid_includes.html %}
7
+ {% include katex_includes.html %}
@@ -4,7 +4,7 @@
4
4
  KaTeX can be enabled with page.katex.
5
5
  -->
6
6
 
7
- {% if page.katex %}
7
+ {% if page.katex || site.mermaid %}
8
8
 
9
9
  <!-- See the releases tab on https://github.com/KaTeX/KaTeX -->
10
10
  <link
@@ -0,0 +1,24 @@
1
+
2
+ <!--
3
+ Include mermaid-js scripts.
4
+ -->
5
+ {% if page.mermaid || site.mermaid %}
6
+ <script>
7
+ var MERMAID_CONFIG = window.MERMAID_CONFIG ?? {{ site.mermaid_config | jsonify }} ?? { };
8
+
9
+ // Load defaults
10
+ MERMAID_CONFIG.startOnLoad ??= true;
11
+ MERMAID_CONFIG.htmlLabels ??= true;
12
+ </script>
13
+ <script
14
+ defer
15
+ src="{{ "assets/plugin/mermaid/mermaid.js" | relative_url }}"
16
+ {% comment %}
17
+ Here, we're also calling "mermaid.init()" because of the defer attribute — it's
18
+ possible that the window has already loaded (and thus startOnLoad may have no effect).
19
+ {% endcomment %}
20
+ onload="mermaid.initialize(MERMAID_CONFIG); mermaid.init();" >
21
+ </script>
22
+
23
+ {% endif %}
24
+
@@ -17,7 +17,7 @@ title: Untitled
17
17
  {% endif %}
18
18
  <script type="module" src="{{ "assets/js/main.mjs" | relative_url }}"></script>
19
19
 
20
- {% include katex_includes.html %}
20
+ {% include extern_library_imports.html %}
21
21
 
22
22
  <title>{{ page.title }} — {{ site.title }}</title>
23
23
  </head>
@@ -0,0 +1,77 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+
5
+ <!-- Defines a https://remarkjs.com/ slideshow -->
6
+
7
+ {% assign frame_resource_url = 'assets/html/remark_presentation_frame.html.resource' | relative_url %}
8
+
9
+ <main class="slideshow-mode">
10
+ <iframe
11
+ id="presentation_frame"
12
+ onload="window.presentationFrameLoaded = true;"></iframe>
13
+ </main>
14
+
15
+ <script defer type="module">
16
+ import slideshow from "{{ 'assets/js/layout/remark_slideshow.mjs' | relative_url }}";
17
+
18
+ let presentationFrame = document.querySelector("#presentation_frame");
19
+ presentationFrame.src = {{ frame_resource_url | jsonify }};
20
+ window.presentationFrameLoaded = false;
21
+
22
+ let config = {{ page.remark_presentation_config | default: site.remark_presentation_config | jsonify }};
23
+ config ??= {};
24
+
25
+ config.source = {{ page.content | jsonify }};
26
+
27
+ // Library imports intended for this page: Forward to the frame.
28
+ let libraryImports = unescape(`
29
+ {% capture library_imports %}
30
+ {% include extern_library_imports.html %}
31
+ {% endcapture %}
32
+ {{ library_imports | url_encode | replace: "+", " " }}
33
+ `);
34
+
35
+ // To be called after the presentation iframe loads.
36
+ window.initSlides = () => {
37
+ let presentationDoc = presentationFrame.contentDocument;
38
+ let presentationWin = presentationFrame.contentWindow;
39
+
40
+ // Ensure that onload doesn't get called again if the frame reloads.
41
+ presentationFrame.onload = null;
42
+
43
+ // Re-create the document, add the library imports to it.
44
+ let slideshowHtml = presentationDoc.documentElement.outerHTML;
45
+
46
+ presentationDoc.open();
47
+ presentationDoc.write("<!DOCTYPE html>");
48
+ presentationDoc.write(slideshowHtml);
49
+ presentationDoc.write(libraryImports);
50
+ presentationDoc.write(`
51
+ <${"script"}>
52
+ if (window.MERMAID_CONFIG) {
53
+ window.MERMAID_CONFIG.useMaxWidth = true;
54
+ window.MERMAID_CONFIG.cloneCssStyles = false;
55
+ window.MERMAID_CONFIG.flowchart = { useMaxWidth: true };
56
+ }
57
+ </${"script"}>
58
+ `);
59
+ presentationDoc.close();
60
+ (async () => {
61
+ await slideshow.start(presentationFrame.contentWindow, config);
62
+
63
+ // Ensure that mermaid has already been run!
64
+ if (presentationWin.mermaid) {
65
+ presentationWin.mermaid.init();
66
+ console.log("Ran mermaid!");
67
+ }
68
+ })();
69
+ };
70
+
71
+ if (window.presentationFrameLoaded) {
72
+ initSlides();
73
+ }
74
+ else {
75
+ presentationFrame.onload = initSlides;
76
+ }
77
+ </script>
data/_sass/_hljs.scss ADDED
@@ -0,0 +1,36 @@
1
+
2
+ // Created while referencing rouge-github.scss as published
3
+ // at https://github.com/pages-themes/cayman/blob/master/_sass/rouge-github.scss
4
+
5
+ .hljs {
6
+ color: var(--syntax-normal-fg);
7
+ display: block;
8
+ white-space: pre-wrap;
9
+
10
+ // Keywords and literals
11
+ .hljs-keyword, .hljs-literal {
12
+ color: var(--syntax-keyword-fg);
13
+ }
14
+
15
+ .hljs-literal {
16
+ font-weight: bold;
17
+ color: var(--syntax-name-fg);
18
+ }
19
+
20
+ // Strings
21
+ .hljs-string {
22
+ color: var(--syntax-string-fg);
23
+ }
24
+
25
+ // Numbers
26
+ .hljs-number {
27
+ color: var(--syntax-number-fg);
28
+ }
29
+
30
+
31
+ // Comments
32
+ .hljs-comment {
33
+ color: var(--syntax-comment-fg);
34
+ font-style: italic;
35
+ }
36
+ }
data/_sass/_layout.scss CHANGED
@@ -154,6 +154,21 @@ nav#post_next_prev {
154
154
  flex-grow: 1;
155
155
  }
156
156
 
157
+ main.slideshow-mode {
158
+ position: absolute;
159
+ display: flex;
160
+
161
+ left: 0;
162
+ right: 0;
163
+ top: var(--header-effective-height);
164
+ bottom: 0;
165
+
166
+ iframe {
167
+ flex-grow: 1;
168
+ border: none;
169
+ }
170
+ }
171
+
157
172
  // On mobile devices,
158
173
  @media screen and (max-width: $site-content-preferred-width) {
159
174
  .main-container {
data/_sass/_nav.scss CHANGED
@@ -41,6 +41,14 @@ body > header {
41
41
  }
42
42
  }
43
43
 
44
+ :root {
45
+ --header-effective-height: var(--header-height);
46
+ }
47
+
48
+ :root.minimizedNavHeader {
49
+ --header-effective-height: 0px;
50
+ }
51
+
44
52
  :root.minimizedNavHeader body > header {
45
53
  > *:not(#toggle_sidebar_btn) {
46
54
  opacity: 0;
data/_sass/hematite.scss CHANGED
@@ -12,3 +12,4 @@
12
12
  @import "_layout";
13
13
  @import "_elements";
14
14
  @import "_rogue";
15
+ @import "_hljs";
@@ -0,0 +1,69 @@
1
+ ---
2
+ ---
3
+ <!-- Using a .html.resource file to prevent Jekyll from changing the extension -->
4
+
5
+ <!DOCTYPE html>
6
+ <html class="lightTheme" lang="{{ page.lang | default: site.lang | default: "en-US" }}">
7
+ <head>
8
+ <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
9
+ <meta charset="utf-8"/>
10
+ <!-- Content of remark presentation iframe. Based on https://github.com/gnab/remark/wiki#getting-started -->
11
+ <style>
12
+ /* Stylesheet mostly taken from https://github.com/gnab/remark/wiki#getting-started */
13
+ @import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz);
14
+ @import url(https://fonts.googleapis.com/css?family=Droid+Serif:400,700,400italic);
15
+ @import url(https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700,400italic);
16
+
17
+ body { font-family: 'Droid Serif'; }
18
+ h1, h2, h3 {
19
+ font-family: 'Yanone Kaffeesatz';
20
+ font-weight: normal;
21
+ }
22
+
23
+ .remark-code, .remark-inline-code { font-family: 'Ubuntu Mono'; }
24
+
25
+ /* Show hidden slides to allow preprocessors (e.g. mermaid) to
26
+ properly account for container size.
27
+ */
28
+ @media screen {
29
+ .remark-slide-container:not(.remark-visible) {
30
+ display: block;
31
+ box-shadow: none;
32
+ z-index: -999;
33
+ visibility: hidden;
34
+ }
35
+ }
36
+
37
+ nav {
38
+ display: flex;
39
+
40
+ position: absolute;
41
+ z-index: 10000;
42
+ bottom: 0;
43
+ left: 0;
44
+ right: 0;
45
+ opacity: 0.8;
46
+ }
47
+
48
+ nav:hover, nav:focus-within {
49
+ opacity: 1;
50
+ }
51
+
52
+ @media print {
53
+ nav {
54
+ display: none;
55
+ }
56
+
57
+ /* Prevent parts of slides from being cut off on Firefox */
58
+ .remark-slide-scaler {
59
+ transform: none !important;
60
+ }
61
+ }
62
+
63
+ </style>
64
+ <link rel="stylesheet" href="{{ 'assets/style_only_syntax.css' | relative_url }}" />
65
+ </head>
66
+ <body>
67
+ </body>
68
+ <script src="{{ 'assets/plugin/remark_presenter/remark.min.js' | relative_url }}"></script>
69
+ </html>
@@ -62,6 +62,22 @@ var UrlHelper = {
62
62
  return url.substring(hashLoc, argsStart);
63
63
  },
64
64
 
65
+ // Replace an existing hash with a [newHash]. The new hash
66
+ //
67
+ withReplacedHash(url, newHash) {
68
+ let hashLoc = url.lastIndexOf('#');
69
+
70
+ if ((newHash + "").charAt(0) != '#') {
71
+ newHash = `#${newHash}`;
72
+ }
73
+
74
+ if (hashLoc == -1) {
75
+ return url + newHash;
76
+ }
77
+
78
+ return url.substring(0, hashLoc) + newHash;
79
+ },
80
+
65
81
  /// Remove metadata encoded in the given URL and returns
66
82
  /// it.
67
83
  /// If [url] is undefined, uses the page's URL.
@@ -115,4 +131,8 @@ assertEq("Trimming metadata",
115
131
  "https://example.com/"
116
132
  );
117
133
 
134
+ assertEq("Replacing a hash",
135
+ UrlHelper.withReplacedHash("https://example.com/#foo", "bar"),
136
+ "https://example.com/#bar");
137
+
118
138
  export default UrlHelper;
@@ -51,7 +51,8 @@ function expandBasedOnURL() {
51
51
  const doExpansion = (url) => {
52
52
  // Determine the hash.
53
53
  let hash = UrlHelper.getPageHash();
54
- if (hash == null) {
54
+ let isInvalid = hash && /^\#\d+/.exec(hash);
55
+ if (hash == null || isInvalid) {
55
56
  return;
56
57
  }
57
58
 
@@ -0,0 +1,164 @@
1
+ import { getUrlQuery, Searcher } from "../search.mjs";
2
+ import { stringLookup } from "../strings.mjs";
3
+ import UrlHelper from "../UrlHelper.mjs";
4
+
5
+ function isInPresenterMode(targetDoc) {
6
+ return targetDoc.body.classList.contains("remark-presenter-mode");
7
+ }
8
+
9
+ function focusSearchResult(targetWin, query, targetMatchNo, slideshow, searcher) {
10
+ let matchesFound = 0;
11
+ for (const slide of slideshow.getSlides()) {
12
+ let targetText = (slide.content ?? []).join("\n");
13
+ let currentSlideNo = slide.getSlideIndex() + 1;
14
+
15
+ matchesFound += searcher.getNumberOfMatches(query, targetText);
16
+ console.log("Considering ", targetText, " with ", matchesFound, "matches found so far");
17
+
18
+ if (matchesFound > targetMatchNo) {
19
+ slideshow.gotoSlide(currentSlideNo);
20
+ return;
21
+ }
22
+
23
+ // Now search the notes!
24
+ if (slide.notes) {
25
+ targetText = slide.notes.join("\n");
26
+ matchesFound += searcher.getNumberOfMatches(query, targetText);
27
+ if (matchesFound > targetMatchNo) {
28
+ slideshow.gotoSlide(currentSlideNo);
29
+
30
+ if (!isInPresenterMode(targetWin.document)) {
31
+ slideshow.togglePresenterMode();
32
+ }
33
+ return;
34
+ }
35
+ }
36
+ }
37
+
38
+ console.log("Found ", matchesFound, " matches, which is less than the target of ", targetMatchNo);
39
+ slideshow.gotoLastSlide();
40
+ return -1;
41
+ }
42
+
43
+ /// Show a search result the user requested through the
44
+ /// page's URL.
45
+ function focusSearchResultFromUrl(targetWin, slideshow) {
46
+ let { query, resultIndex } = getUrlQuery() ?? {};
47
+ let index = resultIndex;
48
+
49
+ if (query === undefined || index === undefined) {
50
+ return;
51
+ }
52
+
53
+ console.log("Focusing a search result: ", query, index);
54
+
55
+ let searcher = new Searcher();
56
+ focusSearchResult(targetWin, query, index, slideshow, searcher);
57
+ }
58
+
59
+ function focusSlideFromHash(slideshow) {
60
+ let hash = UrlHelper.getPageHash();
61
+ if (!hash) {
62
+ return;
63
+ }
64
+
65
+ let targetSlide = parseInt(hash.substring(1));
66
+ if (targetSlide) {
67
+ console.log("Navigating to slide", targetSlide);
68
+ slideshow.gotoSlide(targetSlide);
69
+ }
70
+ }
71
+
72
+ async function main(targetWindow, config) {
73
+ if (!targetWindow.remark) {
74
+ // Wait for page load if remark isn't available yet.
75
+ await (new Promise(resolve => {
76
+ targetWindow.addEventListener('load', resolve);
77
+ }));
78
+ }
79
+
80
+ // See https://remarkjs.com/#8
81
+ let slideshow = targetWindow.remark.create(config);
82
+
83
+ // For debugging
84
+ window.slideshow_debug = slideshow;
85
+
86
+ targetWindow.focus();
87
+
88
+ addExtendedControls(targetWindow, slideshow);
89
+ focusSearchResultFromUrl(targetWindow, slideshow);
90
+
91
+ targetWindow.history.replaceState(null, targetWindow.location.href);
92
+ let targetWinHistory = targetWindow.history.state;
93
+
94
+ focusSlideFromHash(slideshow);
95
+
96
+ window.addEventListener('hashchange', () => {
97
+ focusSlideFromHash(slideshow);
98
+ });
99
+
100
+ slideshow.on('showSlide', function(newSlide) {
101
+ if (!newSlide) {
102
+ return;
103
+ }
104
+ let hashId = newSlide.getSlideIndex() + 1;
105
+
106
+ // Update the window's URL to match that of the interior
107
+ // (e.g. slide 3 = #3).
108
+ // Try not to have the forward/back arrows go forward/back in the iframe's history.
109
+ targetWindow.history.replaceState(
110
+ targetWinHistory,
111
+ UrlHelper.withReplacedHash(targetWindow.location.href, hashId));
112
+ window.location.hash = hashId;
113
+ });
114
+ }
115
+
116
+ /// Apply minor adjustments to the default remark layout
117
+ function addExtendedControls(targetWindow, slideshow) {
118
+ let slideContainer = targetWindow.document.querySelector(".remark-slides-area");
119
+
120
+ // Announce changes to the slide (e.g. going to the next slide).
121
+ slideContainer.setAttribute("aria-live", "polite");
122
+
123
+ // Add next/previous buttons
124
+ let nextSlideBtn = targetWindow.document.createElement("button");
125
+ let prevSlideBtn = targetWindow.document.createElement("button");
126
+ let printBtn = targetWindow.document.createElement("button");
127
+ let spacer = targetWindow.document.createElement("div");
128
+
129
+ let nav = targetWindow.document.createElement("nav");
130
+
131
+ nextSlideBtn.innerText = stringLookup('btn_next_slide');
132
+ prevSlideBtn.innerText = stringLookup('btn_prev_slide');
133
+ printBtn.innerText = stringLookup('btn_print');
134
+
135
+ spacer.style.flexGrow = 1;
136
+
137
+ nextSlideBtn.onclick = () => {
138
+ slideshow.gotoNextSlide();
139
+ };
140
+
141
+ prevSlideBtn.onclick = () => {
142
+ slideshow.gotoPreviousSlide();
143
+ };
144
+
145
+ printBtn.onclick = () => {
146
+ targetWindow.print();
147
+ };
148
+
149
+ slideshow.on('showSlide', function(newSlide) {
150
+ if (!newSlide) {
151
+ return;
152
+ }
153
+
154
+ prevSlideBtn.disabled = (newSlide.getSlideIndex() == 0);
155
+ nextSlideBtn.disabled = (newSlide.getSlideIndex() + 1 >= slideshow.getSlideCount());
156
+ });
157
+
158
+
159
+
160
+ nav.replaceChildren(prevSlideBtn, nextSlideBtn, spacer, printBtn);
161
+ targetWindow.document.body.appendChild(nav);
162
+ }
163
+
164
+ export default { start: main };
data/assets/js/search.mjs CHANGED
@@ -54,6 +54,25 @@ class Searcher {
54
54
 
55
55
  }
56
56
 
57
+ /// Get number of full matches for [query] in [text].
58
+ /// @precondition [text] is already in a searchable (i.e.
59
+ /// filtered) form.
60
+ getNumberOfMatches(query, text) {
61
+ query = query.toLowerCase();
62
+
63
+ return text.toLowerCase().split(query).length - 1;
64
+ }
65
+
66
+ /// Get the index of the first full match for [query] in
67
+ /// [text], starting at [startPos]. Returns -1 if no match
68
+ /// is found.
69
+ /// @precondition [text] is already in a searchable form.
70
+ getIdxOfFirstMatch(query, text, startPos) {
71
+ query = query.toLowerCase();
72
+
73
+ return text.toLowerCase().indexOf(query, startPos);
74
+ }
75
+
57
76
  /// Get the container element for the [n]th search
58
77
  /// result for [query] in the given [elem].
59
78
  /// Returns an Element.
@@ -77,7 +96,7 @@ class Searcher {
77
96
 
78
97
  // If the current node is a leaf,
79
98
  if (elem.childNodes.length == 0) {
80
- let numMatches = searchText.split(query).length - 1;
99
+ let numMatches = this.getNumberOfMatches(query, searchText);
81
100
  n -= numMatches;
82
101
 
83
102
  // If we've considered enough matches,
@@ -154,7 +173,7 @@ class Searcher {
154
173
  index ++;
155
174
  pageData.numMatches ++;
156
175
  startPos = matchLoc + query.length;
157
- matchLoc = toSearch.indexOf(query, startPos);
176
+ matchLoc = this.getIdxOfFirstMatch(query, toSearch, startPos);
158
177
  }
159
178
  }
160
179
 
@@ -185,26 +204,52 @@ class Searcher {
185
204
  }
186
205
  }
187
206
 
207
+ /// Extract a search query and index from the current page's URL.
208
+ function getUrlQuery() {
209
+ let query, resultIndex;
210
+
211
+ let urlArgs = UrlHelper.getPageArgs();
212
+ let pageHash = UrlHelper.getPageHash();
213
+
214
+ if (urlArgs === null) {
215
+ return;
216
+ }
217
+
218
+ // The page's hash also causes scrolling. Don't focus
219
+ // if the page has a hash.
220
+ if (pageHash != null) {
221
+ return;
222
+ }
223
+
224
+ query = urlArgs.query;
225
+ resultIndex = parseInt(urlArgs.index);
226
+
227
+ if (urlArgs.index === undefined) {
228
+ resultIndex = 0;
229
+ }
230
+
231
+ return { query, resultIndex };
232
+ }
233
+
188
234
  /// Scrolls to and shows the search result for the given [query]
189
235
  /// If neither [query] nor [resultIndex] are given, attempt to get them from
190
236
  /// the page's arguments (i.e. from https://example.com/...?search=...,n=...).
191
237
  function focusSearchResult(searcher, elem, query, resultIndex) {
192
- if (query === undefined && resultIndex === undefined) {
193
- let urlArgs = UrlHelper.getPageArgs();
194
- let pageHash = UrlHelper.getPageHash();
238
+ if (elem === undefined) {
239
+ console.warn(`focusSearchResult requires [elem] to function. Not focusing a result.`);
240
+ return;
241
+ }
195
242
 
196
- if (urlArgs === null) {
197
- return;
198
- }
243
+ if (query === undefined && resultIndex === undefined) {
244
+ let pageArgs = getUrlQuery();
199
245
 
200
- // The page's hash also causes scrolling. Don't focus
201
- // if the page has a hash.
202
- if (pageHash != null) {
246
+ // No args, nothing to focus.
247
+ if (!pageArgs) {
203
248
  return;
204
249
  }
205
250
 
206
- query = urlArgs.query;
207
- resultIndex = parseInt(urlArgs.index);
251
+ resultIndex = pageArgs.resultIndex;
252
+ query = pageArgs.query;
208
253
 
209
254
  if (isNaN(resultIndex)) {
210
255
  console.warn("Unable to navigate to result. Given idx is NaN");
@@ -355,4 +400,4 @@ function handleSearch(searcher) {
355
400
  }
356
401
 
357
402
  export default handleSearch;
358
- export { handleSearch, Searcher };
403
+ export { handleSearch, getUrlQuery, Searcher };