hematite 0.0.1
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 +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;
|