hematite 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +58 -0
  4. data/_config.yml +33 -0
  5. data/_data/strings/en.yml +32 -0
  6. data/_data/strings/es.yml +33 -0
  7. data/_includes/img/hamburger_menu.svg +78 -0
  8. data/_includes/img/search_icon.svg +99 -0
  9. data/_includes/katex_includes.html +26 -0
  10. data/_includes/nav/page_navigation.html +10 -0
  11. data/_includes/nav/pages_list.html +17 -0
  12. data/_includes/nav/pinned_page.html +12 -0
  13. data/_includes/nav/sidebar.html +25 -0
  14. data/_layouts/calendar.html +36 -0
  15. data/_layouts/default.html +31 -0
  16. data/_layouts/page.html +5 -0
  17. data/_layouts/post.html +42 -0
  18. data/_sass/_animations.scss +16 -0
  19. data/_sass/_calendar.scss +63 -0
  20. data/_sass/_colors.scss +73 -0
  21. data/_sass/_elements.scss +125 -0
  22. data/_sass/_layout.scss +224 -0
  23. data/_sass/_nav.scss +180 -0
  24. data/_sass/_rogue.scss +50 -0
  25. data/_sass/_sizes.scss +18 -0
  26. data/_sass/hematite.scss +10 -0
  27. data/assets/html/all_tags.html +26 -0
  28. data/assets/img/favicon.svg +12 -0
  29. data/assets/js/AnimationUtil.mjs +72 -0
  30. data/assets/js/AsyncUtil.mjs +18 -0
  31. data/assets/js/DateUtil.mjs +123 -0
  32. data/assets/js/PageAlert.mjs +143 -0
  33. data/assets/js/UrlHelper.mjs +118 -0
  34. data/assets/js/assertions.mjs +9 -0
  35. data/assets/js/dropdownExpander.mjs +78 -0
  36. data/assets/js/layout/calendar.mjs +478 -0
  37. data/assets/js/layout/post.mjs +65 -0
  38. data/assets/js/linkButtonGenerator.mjs +45 -0
  39. data/assets/js/main.mjs +19 -0
  40. data/assets/js/search.mjs +358 -0
  41. data/assets/js/sidebar.mjs +97 -0
  42. data/assets/js/string_data.mjs +19 -0
  43. data/assets/js/strings.mjs +167 -0
  44. data/assets/plugin/katex/README.md +119 -0
  45. data/assets/plugin/katex/contrib/auto-render.min.js +1 -0
  46. data/assets/plugin/katex/contrib/copy-tex.min.css +1 -0
  47. data/assets/plugin/katex/contrib/copy-tex.min.js +1 -0
  48. data/assets/plugin/katex/contrib/mathtex-script-type.min.js +1 -0
  49. data/assets/plugin/katex/contrib/mhchem.min.js +1 -0
  50. data/assets/plugin/katex/contrib/render-a11y-string.min.js +1 -0
  51. data/assets/plugin/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  52. data/assets/plugin/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  53. data/assets/plugin/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  54. data/assets/plugin/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  55. data/assets/plugin/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  56. data/assets/plugin/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  57. data/assets/plugin/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  58. data/assets/plugin/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  59. data/assets/plugin/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  60. data/assets/plugin/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  61. data/assets/plugin/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  62. data/assets/plugin/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  63. data/assets/plugin/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  64. data/assets/plugin/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  65. data/assets/plugin/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  66. data/assets/plugin/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  67. data/assets/plugin/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  68. data/assets/plugin/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  69. data/assets/plugin/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  70. data/assets/plugin/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  71. data/assets/plugin/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  72. data/assets/plugin/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  73. data/assets/plugin/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  74. data/assets/plugin/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  75. data/assets/plugin/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  76. data/assets/plugin/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  77. data/assets/plugin/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  78. data/assets/plugin/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  79. data/assets/plugin/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  80. data/assets/plugin/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  81. data/assets/plugin/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  82. data/assets/plugin/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  83. data/assets/plugin/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  84. data/assets/plugin/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  85. data/assets/plugin/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  86. data/assets/plugin/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  87. data/assets/plugin/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  88. data/assets/plugin/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  89. data/assets/plugin/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  90. data/assets/plugin/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  91. data/assets/plugin/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  92. data/assets/plugin/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  93. data/assets/plugin/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  94. data/assets/plugin/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  95. data/assets/plugin/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  96. data/assets/plugin/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  97. data/assets/plugin/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  98. data/assets/plugin/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  99. data/assets/plugin/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  100. data/assets/plugin/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  101. data/assets/plugin/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  102. data/assets/plugin/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  103. data/assets/plugin/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  104. data/assets/plugin/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  105. data/assets/plugin/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  106. data/assets/plugin/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  107. data/assets/plugin/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  108. data/assets/plugin/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  109. data/assets/plugin/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  110. data/assets/plugin/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  111. data/assets/plugin/katex/katex.min.css +1 -0
  112. data/assets/plugin/katex/katex.min.js +1 -0
  113. data/assets/search_data.json +36 -0
  114. data/assets/style.scss +15 -0
  115. metadata +170 -0
@@ -0,0 +1,72 @@
1
+ import AsyncUtil from "./AsyncUtil.mjs";
2
+
3
+ const DEFAULT_DURATION = 500;
4
+
5
+ /// Get the transition style property for a given [duration].
6
+ function getTransitionStyleStr_(duration) {
7
+ return [
8
+ `height ${duration}ms ease`,
9
+ `transform ${duration}ms ease`,
10
+ `margin ${duration}ms ease`,
11
+ ].join(', ');
12
+ }
13
+
14
+ /// Vertically collapse, then hide, [elem].
15
+ /// Animation [duration] is in milliseconds and optional.
16
+ /// Ultimately, [elem].display.
17
+ ///
18
+ /// Clients should not rely on [elem].style for otherwise
19
+ /// styling [elem].
20
+ async function collapseOutVert(elem, duration) {
21
+ duration ??= DEFAULT_DURATION;
22
+
23
+ elem.style.height = `${elem.clientHeight}px`;
24
+ elem.style.transition = getTransitionStyleStr_(duration);
25
+ elem.style.display = getComputedStyle(elem).display;
26
+
27
+ await AsyncUtil.nextAnimationFrame();
28
+
29
+ elem.style.height = 0;
30
+ elem.style.margin = 0;
31
+ await AsyncUtil.waitMillis(duration);
32
+ elem.style.display = `none`;
33
+
34
+ elem.style.height = ``;
35
+ elem.style.transition = ``;
36
+ elem.style.margin = ``;
37
+ }
38
+
39
+ /// Vertically expand [elem]
40
+ async function expandInVert(elem, duration) {
41
+ duration ??= DEFAULT_DURATION;
42
+ elem.style.display = ``;
43
+ elem.style.transition = getTransitionStyleStr_(duration);
44
+
45
+ // Determine the true size of the element
46
+ elem.style.height = ``;
47
+ elem.style.margin = 0;
48
+ elem.style.opacity = 0;
49
+ elem.style.position = 'absolute';
50
+ elem.style.visibility = 'hidden';
51
+
52
+ // Allow the browser to lay out the element.
53
+ await AsyncUtil.nextAnimationFrame();
54
+
55
+ let finalHeight = elem.clientHeight;
56
+ elem.style.height = 0;
57
+
58
+ await AsyncUtil.nextAnimationFrame();
59
+
60
+ elem.style.visibility = '';
61
+ elem.style.position = '';
62
+ elem.style.opacity = '';
63
+ elem.style.height = `${finalHeight}px`;
64
+ elem.style.margin = '';
65
+
66
+ await AsyncUtil.waitMillis(duration);
67
+
68
+ elem.style.height = ``;
69
+ elem.style.transition = ``;
70
+ }
71
+
72
+ export default { collapseOutVert, expandInVert };
@@ -0,0 +1,18 @@
1
+
2
+ const AsyncUtil = {
3
+ /// Resolves on the next animation frame
4
+ nextAnimationFrame() {
5
+ return new Promise(resolve => {
6
+ requestAnimationFrame(() => resolve());
7
+ });
8
+ },
9
+
10
+ /// Resolve in [duration] milliseconds
11
+ waitMillis(duration) {
12
+ return new Promise(resolve => {
13
+ setTimeout(() => resolve(), duration);
14
+ });
15
+ },
16
+ };
17
+
18
+ export default AsyncUtil;
@@ -0,0 +1,123 @@
1
+ ---
2
+ ---
3
+
4
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
5
+ const MS_PER_WEEK = MS_PER_DAY * 7;
6
+
7
+ var DateUtil = {
8
+ MS_PER_DAY,
9
+ MS_PER_WEEK,
10
+
11
+ /// Returns [true] iff [date] is today.
12
+ dateIsToday(date) {
13
+ let now = new Date();
14
+
15
+ return DateUtil.datesAreOnSameDay(now, date);
16
+ },
17
+
18
+ /// Returns [true] iff [a] and [b] refer to the same day.
19
+ datesAreOnSameDay(a, b) {
20
+ return a.getDate() == b.getDate()
21
+ && a.getFullYear() == b.getFullYear()
22
+ && a.getMonth() == b.getMonth();
23
+ },
24
+
25
+ /// Returns a copy of [date] that points to the next day.
26
+ nextDay(date) {
27
+ return new Date(date.getTime() + MS_PER_DAY);
28
+ },
29
+
30
+ prevDay(date) {
31
+ return new Date(date.getTime() - MS_PER_DAY);
32
+ },
33
+
34
+ /// Returns a copy of [date] that points to the next week.
35
+ nextWeek(date) {
36
+ return new Date(date.getTime() + MS_PER_WEEK);
37
+ },
38
+
39
+ prevWeek(date) {
40
+ return new Date(date.getTime() - MS_PER_WEEK);
41
+ },
42
+
43
+ /// Return a copy of [date] that points to the next month.
44
+ nextMonth(date) {
45
+ let res = new Date(date);
46
+
47
+ if (res.getMonth() == 11) {
48
+ res.setYear(res.getYear() + 1);
49
+ res.setMonth(0);
50
+ }
51
+ else {
52
+ res.setMonth(res.getMonth() + 1);
53
+ }
54
+
55
+ return res;
56
+ },
57
+
58
+ prevMonth(date) {
59
+ let res = new Date(date);
60
+
61
+ if (res.getMonth() == 0) {
62
+ res.setYear(res.getYear() - 1);
63
+ res.setMonth(11);
64
+ }
65
+ else {
66
+ res.setMonth(res.getMonth() - 1);
67
+ }
68
+
69
+ return res;
70
+ },
71
+
72
+ /// Returns a Date on the beginning of the week containing [date].
73
+ beginningOfWeek(date) {
74
+ let dayOfWeek = date.getDay();
75
+
76
+ return new Date(date.getTime() - MS_PER_DAY * dayOfWeek);
77
+ },
78
+
79
+ beginningOfMonth(date) {
80
+ let dayOfMonth = date.getDate() - 1;
81
+
82
+ return new Date(date.getTime() - MS_PER_DAY * dayOfMonth);
83
+ },
84
+
85
+ /// Iterator of days in the range from [start] up to [stop]
86
+ /// inclusive.
87
+ *daysInRange(start, stop) {
88
+ let current = start;
89
+
90
+ for (; current < stop; current = DateUtil.nextDay(current)) {
91
+ yield current;
92
+ }
93
+ },
94
+
95
+ /// Slightly more intelligent date parsing than new Date(string).
96
+ parse(text) {
97
+ // Remove -th, -rd, -ero suffexes
98
+ text = text.replaceAll(/(\d)(?:rd|th|ero)/g,
99
+ (fullMatch, group0) => group0);
100
+ console.log("Parsing", text);
101
+
102
+ return new Date(text);
103
+ },
104
+
105
+ /// Returns true iff the given object is a date object.
106
+ isDate(obj) {
107
+ return typeof (obj) == "object" && obj.__proto__ == (new Date()).__proto__;
108
+ },
109
+
110
+ /// Converts the given date to a preferences-based localized string
111
+ toString(date, dateOptions) {
112
+ dateOptions ??= {{ site.hematite.date_format | default: nil | jsonify }};
113
+ // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
114
+ // for other formatting options.
115
+ dateOptions ??= {
116
+ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
117
+ };
118
+
119
+ return date.toLocaleString(undefined, dateOptions);
120
+ },
121
+ };
122
+
123
+ export default DateUtil;
@@ -0,0 +1,143 @@
1
+ /// Builds an alert message that appears at the bottom of the page.
2
+
3
+ const PAGE_ALERT_DEFAULT_TIMEOUT = 5000; // ms
4
+ const PAGE_ALERT_FAST_TIMEOUT = 250;
5
+ const PAGE_ALERT_DIALOG_CLSS = "pageAlert";
6
+ const PAGE_ALERT_DIALOG_HIDDEN_CLSS = "hidden";
7
+
8
+ class PageAlertBuilder {
9
+ constructor() {
10
+ this.text_ = "";
11
+ this.timeout_ = PAGE_ALERT_DEFAULT_TIMEOUT;
12
+ }
13
+
14
+ /// Sets the alert's text to [text]
15
+ withText(text) {
16
+ this.text_ = text;
17
+
18
+ return this;
19
+ }
20
+
21
+ /// Destroys the alert after [timeout] ms instead of the
22
+ /// default.
23
+ withTimeout(timeout) {
24
+ this.timeout_ = timeout;
25
+
26
+ return this;
27
+ }
28
+
29
+ /// Keep the dialog open after its initial appearance.
30
+ withoutTimeout() {
31
+ this.timeout_ = -1;
32
+
33
+ return this;
34
+ }
35
+
36
+ /// Don't show the announcement to the user. Useful for accessibility-
37
+ ///related announcements.
38
+ invisible() {
39
+ this.invisible_ = true;
40
+
41
+ return this;
42
+ }
43
+
44
+ /// Build, but don't yet show the alert. Returns an object
45
+ /// with [show] and [destroy] methods.
46
+ build() {
47
+ let destroyTimeout = -1;
48
+ let dialog = document.createElement("div");
49
+ let messageArea = document.createElement("div");
50
+
51
+ dialog.classList.add(PAGE_ALERT_DIALOG_CLSS);
52
+ dialog.setAttribute('role', 'alert');
53
+
54
+ messageArea.innerText = this.text_;
55
+ dialog.appendChild(messageArea);
56
+
57
+ if (this.invisible_) {
58
+ dialog.classList.add(PAGE_ALERT_DIALOG_HIDDEN_CLSS);
59
+ dialog.style.opacity = 0;
60
+ dialog.style.pointerEvents = 'none';
61
+ console.log("PageAlert-invisible", this.text_);
62
+ }
63
+
64
+ /// Removes the dialog.
65
+ let destroying = false;
66
+ let destroy = () => {
67
+ if (destroying) {
68
+ return;
69
+ }
70
+
71
+ // Fade out.
72
+ dialog.classList.add("closing");
73
+ destroying = true;
74
+
75
+ requestAnimationFrame(() => {
76
+ let animDurationStr = getComputedStyle(dialog).getPropertyValue("animation-duration");
77
+ let destroyAnimationDuration;
78
+
79
+ // Get the duration in milliseconds.
80
+ let secMatches = /^(\d+\.?\d*)s$/.exec(animDurationStr);
81
+ let millisMatches = /^(\d+)ms$/.exec(animDurationStr);
82
+
83
+ if (secMatches) {
84
+ destroyAnimationDuration = parseFloat(secMatches[1]) * 1000;
85
+ }
86
+ else if (millisMatches) {
87
+ destroyAnimationDuration = parseInt(millisMatches[1]);
88
+ }
89
+
90
+ // If we weren't able to get a reasonable value
91
+ if (isNaN(destroyAnimationDuration)) {
92
+ console.warn(
93
+ `${animDurationStr} doesn't seem to be a value in milliseconds or seconds. ` +
94
+ `Exit animations for alerts may be disabled`
95
+ );
96
+ destroyAnimationDuration = 0;
97
+ }
98
+
99
+ setTimeout(() => {
100
+ dialog.remove();
101
+ dialog = null;
102
+ destroying = false;
103
+
104
+ if (destroyTimeout !== -1) {
105
+ clearTimeout(destroyTimeout);
106
+ destroyTimeout = -1;
107
+ }
108
+ }, destroyAnimationDuration);
109
+ });
110
+ };
111
+
112
+ return {
113
+ show: () => {
114
+ document.body.appendChild(dialog);
115
+
116
+ if (this.timeout_ != -1) {
117
+ destroyTimeout = setTimeout(() => {
118
+ destroy();
119
+ }, this.timeout_);
120
+ }
121
+ },
122
+ destroy,
123
+ };
124
+ }
125
+ }
126
+
127
+ var PageAlert = {
128
+ builder() {
129
+ return new PageAlertBuilder();
130
+ }
131
+ };
132
+
133
+ /// Short method for creating an invisible alert with [text].
134
+ function announceForAccessibility(text) {
135
+ return PageAlert.builder()
136
+ .withText(text)
137
+ .invisible()
138
+ .withTimeout(PAGE_ALERT_FAST_TIMEOUT)
139
+ .build().show();
140
+ }
141
+
142
+ export { PageAlert, announceForAccessibility };
143
+ export default PageAlert;
@@ -0,0 +1,118 @@
1
+
2
+ import { assertEq } from "./assertions.mjs";
3
+
4
+ var UrlHelper = {
5
+ /// Get arguments in [url].
6
+ ///
7
+ /// [url] defaults to the current page's URL.
8
+ /// (e.g. { foo: 5, bar: 2 } from https://localhost/?foo=5,bar=2).
9
+ getPageArgs(url) {
10
+ url ??= location.href;
11
+
12
+ let argSepIndex = url.lastIndexOf('?');
13
+ let hashLoc = url.lastIndexOf('#');
14
+ if (argSepIndex == -1) {
15
+ return null;
16
+ }
17
+
18
+ // Remove the hash
19
+ if (hashLoc > argSepIndex) {
20
+ url = url.substring(0, hashLoc);
21
+ }
22
+
23
+ let argsSegment = url.substring(argSepIndex + 1);
24
+ let args = argsSegment.split(',');
25
+ let result = {};
26
+
27
+ for (const part of args) {
28
+ let assignIdx = part.indexOf('=');
29
+
30
+ if (assignIdx == -1) {
31
+ result[part] = true;
32
+ }
33
+ else {
34
+ let key = part.substring(0, assignIdx);
35
+ let val = part.substring(assignIdx + 1);
36
+ result[key] = unescape(val);
37
+ }
38
+ }
39
+
40
+ return result;
41
+ },
42
+
43
+ /// Gets the hash (e.g. #foobar from bob.com#foobar?t=5)
44
+ /// for [url] or the current page (if [url] is not given).
45
+ ///
46
+ /// Return value includes the '#' symbol.
47
+ getPageHash(url) {
48
+ url ??= location.href;
49
+
50
+ let hashLoc = url.lastIndexOf('#');
51
+ let argsStart = url.lastIndexOf('?');
52
+
53
+ if (hashLoc == -1) {
54
+ return null;
55
+ }
56
+
57
+ // If there's no '?' or the '?' is before the '#'...
58
+ if (argsStart == -1 || argsStart < hashLoc) {
59
+ argsStart = url.length;
60
+ }
61
+
62
+ return url.substring(hashLoc, argsStart);
63
+ },
64
+
65
+ /// Remove metadata encoded in the given URL and returns
66
+ /// it.
67
+ /// If [url] is undefined, uses the page's URL.
68
+ trimMetadata(url) {
69
+ url ??= location.href;
70
+
71
+ let hashLoc = url.lastIndexOf('#');
72
+ let argsStart = url.lastIndexOf('?');
73
+ let trimTo = Math.min(hashLoc, argsStart);
74
+
75
+ if (trimTo == -1) {
76
+ return url;
77
+ }
78
+
79
+ return url.substring(0, trimTo);
80
+ }
81
+ };
82
+
83
+ assertEq("getPageArgs with no args",
84
+ UrlHelper.getPageArgs("https://example.com/foo/bar"),
85
+ null
86
+ );
87
+
88
+ assertEq("getPageArgs with one arg",
89
+ UrlHelper.getPageArgs("https://example.com/foo?test=3").test,
90
+ "3"
91
+ );
92
+
93
+ assertEq("getPageArgs for page with multiple args and hash",
94
+ UrlHelper.getPageArgs("example.com/#testing?a=5,b=6,asdf=8").b,
95
+ "6"
96
+ );
97
+
98
+ assertEq("getPageHash for no hash page",
99
+ UrlHelper.getPageHash("https://example.com/"),
100
+ null
101
+ );
102
+
103
+ assertEq("getPageHash for page with no args, but a hash",
104
+ UrlHelper.getPageHash("https://www.example.org/foo#header"),
105
+ "#header"
106
+ );
107
+
108
+ assertEq("getPageHash for page with args and hash",
109
+ UrlHelper.getPageHash("example.com/?a=5,b=6,asdf=8#testing"),
110
+ "#testing"
111
+ );
112
+
113
+ assertEq("Trimming metadata",
114
+ UrlHelper.trimMetadata("https://example.com/?a=1#no"),
115
+ "https://example.com/"
116
+ );
117
+
118
+ export default UrlHelper;
@@ -0,0 +1,9 @@
1
+
2
+ /// Throws an error with [description] if [a] !== [b]
3
+ function assertEq(description, a, b) {
4
+ if (a !== b) {
5
+ throw new Error(`Assertion failed (${a} != ${b}): ${description}`);
6
+ }
7
+ }
8
+
9
+ export { assertEq };
@@ -0,0 +1,78 @@
1
+ import UrlHelper from "./UrlHelper.mjs";
2
+
3
+ /// Formats a page for printing
4
+ /// For-printing formatting should be done in CSS, if possible.
5
+
6
+ /// Expands all dropdowns on a page, returns a list of all
7
+ /// dropdowns that were opened.
8
+ function expandAllDropdowns() {
9
+ let expanded = [];
10
+
11
+ for (const dropdown of document.querySelectorAll("details:not([open])")) {
12
+ dropdown.setAttribute("open", true);
13
+ expanded.push(dropdown);
14
+ }
15
+
16
+ return expanded;
17
+ }
18
+
19
+ function expandOnPrint() {
20
+ let openedDropdowns = [];
21
+
22
+ addEventListener("beforeprint", () => {
23
+ openedDropdowns = expandAllDropdowns();
24
+ });
25
+
26
+ addEventListener("afterprint", () => {
27
+ for (const dropdown of openedDropdowns) {
28
+ dropdown.removeAttribute("open");
29
+ }
30
+ });
31
+ }
32
+
33
+ /// Expand all dropdowns containing [elem].
34
+ /// [elem] can be either a Node or an Element.
35
+ function expandContainingDropdowns(elem) {
36
+ let currentElem = elem;
37
+
38
+ // Walk up the DOM tree...
39
+ do {
40
+ // Open all containing details elements.
41
+ if (currentElem.tagName && currentElem.tagName.toLowerCase() == "details") {
42
+ currentElem.setAttribute("open", true);
43
+ }
44
+
45
+ currentElem = currentElem.parentElement;
46
+ }
47
+ while (currentElem);
48
+ }
49
+
50
+ function expandBasedOnURL() {
51
+ const doExpansion = (url) => {
52
+ // Determine the hash.
53
+ let hash = UrlHelper.getPageHash();
54
+ if (hash == null) {
55
+ return;
56
+ }
57
+
58
+ let targetElem = document.querySelector(hash);
59
+ let currentElem = targetElem;
60
+
61
+ expandContainingDropdowns(targetElem);
62
+
63
+ targetElem.focus();
64
+ };
65
+
66
+ doExpansion(location.href);
67
+ addEventListener("hashchange", ({ newURL }) => {
68
+ doExpansion(newURL);
69
+ });
70
+ }
71
+
72
+ function autoExpandDropdowns() {
73
+ expandOnPrint();
74
+ expandBasedOnURL();
75
+ }
76
+
77
+ export { autoExpandDropdowns, expandContainingDropdowns };
78
+ export default autoExpandDropdowns;