hematite 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +58 -0
- data/_config.yml +33 -0
- data/_data/strings/en.yml +32 -0
- data/_data/strings/es.yml +33 -0
- data/_includes/img/hamburger_menu.svg +78 -0
- data/_includes/img/search_icon.svg +99 -0
- data/_includes/katex_includes.html +26 -0
- data/_includes/nav/page_navigation.html +10 -0
- data/_includes/nav/pages_list.html +17 -0
- data/_includes/nav/pinned_page.html +12 -0
- data/_includes/nav/sidebar.html +25 -0
- data/_layouts/calendar.html +36 -0
- data/_layouts/default.html +31 -0
- data/_layouts/page.html +5 -0
- data/_layouts/post.html +42 -0
- data/_sass/_animations.scss +16 -0
- data/_sass/_calendar.scss +63 -0
- data/_sass/_colors.scss +73 -0
- data/_sass/_elements.scss +125 -0
- data/_sass/_layout.scss +224 -0
- data/_sass/_nav.scss +180 -0
- data/_sass/_rogue.scss +50 -0
- data/_sass/_sizes.scss +18 -0
- data/_sass/hematite.scss +10 -0
- data/assets/html/all_tags.html +26 -0
- data/assets/img/favicon.svg +12 -0
- data/assets/js/AnimationUtil.mjs +72 -0
- data/assets/js/AsyncUtil.mjs +18 -0
- data/assets/js/DateUtil.mjs +123 -0
- data/assets/js/PageAlert.mjs +143 -0
- data/assets/js/UrlHelper.mjs +118 -0
- data/assets/js/assertions.mjs +9 -0
- data/assets/js/dropdownExpander.mjs +78 -0
- data/assets/js/layout/calendar.mjs +478 -0
- data/assets/js/layout/post.mjs +65 -0
- data/assets/js/linkButtonGenerator.mjs +45 -0
- data/assets/js/main.mjs +19 -0
- data/assets/js/search.mjs +358 -0
- data/assets/js/sidebar.mjs +97 -0
- data/assets/js/string_data.mjs +19 -0
- data/assets/js/strings.mjs +167 -0
- data/assets/plugin/katex/README.md +119 -0
- data/assets/plugin/katex/contrib/auto-render.min.js +1 -0
- data/assets/plugin/katex/contrib/copy-tex.min.css +1 -0
- data/assets/plugin/katex/contrib/copy-tex.min.js +1 -0
- data/assets/plugin/katex/contrib/mathtex-script-type.min.js +1 -0
- data/assets/plugin/katex/contrib/mhchem.min.js +1 -0
- data/assets/plugin/katex/contrib/render-a11y-string.min.js +1 -0
- data/assets/plugin/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Main-Bold.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Main-Italic.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Main-Regular.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Math-Italic.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Script-Regular.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- data/assets/plugin/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- data/assets/plugin/katex/katex.min.css +1 -0
- data/assets/plugin/katex/katex.min.js +1 -0
- data/assets/search_data.json +36 -0
- data/assets/style.scss +15 -0
- 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,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;
|