@xiee/utils 1.9.5 → 1.10.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.
package/README.md CHANGED
@@ -163,6 +163,11 @@ KaTeX's auto-render extension.
163
163
  Right-align a `<blockquote>` footer if the footer is a `<p>` that starts with
164
164
  the em-dash.
165
165
 
166
+ ## snap.js
167
+
168
+ Create HTML slides using [the CSS Scroll Snap
169
+ technique](https://yihui.org/en/2023/09/snap-slides/).
170
+
166
171
  ## tabsets.js
167
172
 
168
173
  Create tabsets from section headings and their content. See [this
package/css/snap.css ADDED
@@ -0,0 +1,98 @@
1
+ :root { --slide-width: 100%; }
2
+ html { scroll-snap-type: y mandatory; }
3
+ th, td { padding: .2em .5em; }
4
+ .slide { padding: 0 1em; }
5
+ .slide, .frontmatter .main, .middle .main {
6
+ display: flex;
7
+ flex-direction: column;
8
+ }
9
+ .slide > .main { flex-grow: 1; }
10
+ .slide > .header { margin-bottom: 1em; }
11
+ .slide h2, .slide h3 { margin-top: unset; }
12
+ body {
13
+ max-width: fit-content;
14
+ padding: 0;
15
+ }
16
+ a { color: #eb4a47; }
17
+ :not(pre) > code { background-color: #fdfded; }
18
+ #TOC { columns: 2; }
19
+ #TOC::before {
20
+ font-size: 1.3em;
21
+ font-weight: bold;
22
+ display: block;
23
+ border-bottom: 1px solid #666;
24
+ }
25
+ .frontmatter .main, .middle .main {
26
+ justify-content: center;
27
+ }
28
+ .footer {
29
+ display: flex;
30
+ justify-content: space-between;
31
+ opacity: .5;
32
+ font: .7em monospace;
33
+ }
34
+ .inverse {
35
+ background-color: #eee;
36
+ filter: invert(1);
37
+ }
38
+ .fade {
39
+ background: repeating-linear-gradient(135deg, white, white 30px, #ddd 32px, #ddd 32px);
40
+ opacity: 0.6;
41
+ }
42
+ .center { text-align: center; }
43
+ .slide-container h2 .section-number {
44
+ display: inline-block;
45
+ background-color: #666;
46
+ color: white;
47
+ padding: 0 .1em;
48
+ margin-right: .3em;
49
+ }
50
+ .overview { font-size: .8em; }
51
+ .overview .slide-container {
52
+ display: flex;
53
+ flex-wrap: wrap;
54
+ justify-content: space-evenly;
55
+ }
56
+ .overview .slide-container .slide {
57
+ width: var(--slide-width);
58
+ border: 1px dotted #ccc;
59
+ margin-bottom: 0.5em;
60
+ }
61
+ .mirrored { transform: scale(-1, 1); }
62
+ .timer { visibility: hidden; }
63
+ html:fullscreen::-webkit-scrollbar, .spacer { display: none; }
64
+ html:fullscreen {
65
+ -ms-overflow-style: none;
66
+ scrollbar-width: none;
67
+ }
68
+ @media screen and (min-width: 992px) {
69
+ :root {
70
+ --slide-width: 49%;
71
+ --slide-scale: 1;
72
+ --slide-ratio: 0.75;
73
+ --slide-top: auto;
74
+ }
75
+ .slide-mode {
76
+ font-size: 2em;
77
+ background-color: #d7d8d2;
78
+ scale: var(--slide-scale);
79
+ margin-top: var(--slide-top);
80
+ }
81
+ .slide-mode .slide {
82
+ min-height: calc(100vh / var(--slide-scale));
83
+ width: calc(100vh / var(--slide-ratio) / var(--slide-scale));
84
+ box-shadow: 0 0 2em #888;
85
+ clip-path: inset(0 -2em 0 -2em);
86
+ background-color: white;
87
+ scroll-snap-align: start;
88
+ }
89
+ li li { font-size: .9em; }
90
+ .slide-mode .spacer { display: block; }
91
+ .slide-mode .timer { visibility: visible; }
92
+ }
93
+ @media (min-width: 1400px) {
94
+ :root { --slide-width: 33%; }
95
+ }
96
+ @media (min-width: 1800px) {
97
+ :root { --slide-width: 24.67%; }
98
+ }
@@ -0,0 +1 @@
1
+ :root{--slide-width:100%}html{scroll-snap-type:y mandatory}td,th{padding:.2em .5em}.slide{padding:0 1em}.frontmatter .main,.middle .main,.slide{display:flex;flex-direction:column}.slide>.main{flex-grow:1}.slide>.header{margin-bottom:1em}.slide h2,.slide h3{margin-top:unset}body{max-width:fit-content;padding:0}a{color:#eb4a47}:not(pre)>code{background-color:#fdfded}#TOC{columns:2}#TOC::before{font-size:1.3em;font-weight:700;display:block;border-bottom:1px solid #666}.frontmatter .main,.middle .main{justify-content:center}.footer{display:flex;justify-content:space-between;opacity:.5;font:.7em monospace}.inverse{background-color:#eee;filter:invert(1)}.fade{background:repeating-linear-gradient(135deg,#fff,#fff 30px,#ddd 32px,#ddd 32px);opacity:.6}.center{text-align:center}.slide-container h2 .section-number{display:inline-block;background-color:#666;color:#fff;padding:0 .1em;margin-right:.3em}.overview{font-size:.8em}.overview .slide-container{display:flex;flex-wrap:wrap;justify-content:space-evenly}.overview .slide-container .slide{width:var(--slide-width);border:1px dotted #ccc;margin-bottom:.5em}.mirrored{transform:scale(-1,1)}.timer{visibility:hidden}.spacer,html:fullscreen::-webkit-scrollbar{display:none}html:fullscreen{-ms-overflow-style:none;scrollbar-width:none}@media screen and (min-width:992px){:root{--slide-width:49%;--slide-scale:1;--slide-ratio:0.75;--slide-top:auto}.slide-mode{font-size:2em;background-color:#d7d8d2;scale:var(--slide-scale);margin-top:var(--slide-top)}.slide-mode .slide{min-height:calc(100vh / var(--slide-scale));width:calc(100vh / var(--slide-ratio)/ var(--slide-scale));box-shadow:0 0 2em #888;clip-path:inset(0 -2em 0 -2em);background-color:#fff;scroll-snap-align:start}li li{font-size:.9em}.slide-mode .spacer{display:block}.slide-mode .timer{visibility:visible}}@media (min-width:1400px){:root{--slide-width:33%}}@media (min-width:1800px){:root{--slide-width:24.67%}}
package/js/snap.js ADDED
@@ -0,0 +1,193 @@
1
+ (function(d) {
2
+ let p = d.body; // container of slides; assume <body> for now
3
+ const s1 = ':scope > hr:not([class])', s2 = ':scope > h2';
4
+ // find a container that has at least n "slides"
5
+ function findContainer(s, n = 1) {
6
+ if (p.querySelectorAll(s).length >= n) return true;
7
+ // if body doesn't contain headings or <hr>s, look into children
8
+ for (let i = 0; i < p.children.length; i++) {
9
+ if (p.children[i].querySelectorAll(s).length >= n) {
10
+ p = p.children[i]; break;
11
+ }
12
+ }
13
+ return false;
14
+ }
15
+ function newEl(tag, cls) {
16
+ const el = d.createElement(tag);
17
+ if (cls) el.className = cls;
18
+ return el;
19
+ }
20
+ if (!findContainer(s1, 3)) {
21
+ // if not enough <hr>s found in children; look for <h2> instead
22
+ if (p.tagName === 'BODY') {
23
+ // not enough h2 found, this page is not appropriate for slides
24
+ if (!findContainer(s2) && p.tagName === 'BODY') return;
25
+ p.querySelectorAll(s2).forEach(h2 => h2.before(newEl('hr')));
26
+ }
27
+ }
28
+ p.classList.add('slide-container');
29
+ // add 'slide' class to the frontmatter div and toc
30
+ ['.frontmatter', '#TOC'].forEach(sel => {
31
+ const el = d.body.querySelector(sel);
32
+ if (!el) return;
33
+ if (sel === '.frontmatter') {
34
+ el.classList.add('slide');
35
+ } else {
36
+ const s = newSlide(); el.before(s); s.append(el);
37
+ }
38
+ });
39
+
40
+ function newSlide(s) {
41
+ return (s?.innerText === '') ? s : newEl('div', 'slide');
42
+ }
43
+ function isSep(el) {
44
+ return el.tagName === 'HR' && el.attributes.length === 0;
45
+ }
46
+ let el = p.firstElementChild; isSep(el) && el.remove();
47
+ el = p.firstElementChild; if (!el) return;
48
+ let s = newSlide(); el.before(s);
49
+ while (true) {
50
+ let el = s.nextSibling;
51
+ if (!el) break;
52
+ // remove slide separators (<hr>) and create new slide
53
+ if (isSep(el)) {
54
+ s = newSlide(s);
55
+ el.before(s); el.remove();
56
+ } else if (el.classList?.contains('slide')) {
57
+ s = newSlide(s);
58
+ el.after(s);
59
+ } else {
60
+ s.append(el);
61
+ }
62
+ }
63
+ function setAttr(el, attr) {
64
+ const m = newEl('div');
65
+ m.innerHTML = `<div ${attr}></div>`;
66
+ const attrs = m.firstElementChild.attributes;
67
+ for (const a of attrs) {
68
+ el.setAttribute(a.name, a.value);
69
+ }
70
+ m.remove();
71
+ }
72
+ function reveal(el) {
73
+ setTimeout(() => el?.scrollIntoView(), 100);
74
+ }
75
+ const dE = d.documentElement, dC = d.body.classList;
76
+ const slides = d.querySelectorAll('div.slide'), N = slides.length,
77
+ tm = d.querySelector('span.timer'), fn = d.querySelector('.footnotes');
78
+ slides.forEach((s, i) => {
79
+ // slide header, main body, and footer
80
+ const header = newEl('div', 'header'), main = newEl('div', 'main'), footer = newEl('div', 'footer');
81
+ main.append(...s.childNodes);
82
+ s.append(main);
83
+ s.insertAdjacentElement('afterbegin', header);
84
+ s.insertAdjacentElement('beforeend', footer);
85
+ // append footnotes
86
+ if (fn) s.querySelectorAll('.footnote-ref > a[href^="#fn"]').forEach(a => {
87
+ const li = fn.querySelector('li' + a.getAttribute('href'));
88
+ if (!li) return;
89
+ let f = s.querySelector('section.footnotes');
90
+ if (!f) {
91
+ f = newEl('section', 'footnotes'); footer.before(f);
92
+ }
93
+ f.append(li);
94
+ li.firstElementChild?.insertAdjacentHTML('afterbegin', `[${a.innerHTML}] `);
95
+ li.outerHTML = li.innerHTML;
96
+ });
97
+ // add a timer
98
+ footer.append(tm ? tm.cloneNode() : newEl('span', 'timer'));
99
+ // add page numbers
100
+ const n = newEl('span', 'page-number');
101
+ n.innerText = i + 1 + '/' + N;
102
+ n.onclick = e => location.hash = i + 1;
103
+ footer.append(n);
104
+ // apply slide attributes in <!--# -->
105
+ for (const node of main.childNodes) {
106
+ if (node.nodeType !== Node.COMMENT_NODE) continue;
107
+ let t = node.textContent;
108
+ if (!/^#/.test(t)) continue;
109
+ t = t.replace(/^#/, '');
110
+ const r = /[\s\n]class="([^"]+)"/, m = t.match(r);
111
+ if (m) {
112
+ t = t.replace(r, '').trim();
113
+ s.className += ' ' + m[1];
114
+ }
115
+ if (t) setAttr(s, t);
116
+ break;
117
+ }
118
+ s.addEventListener('click', e => {
119
+ e.altKey && (toggleView(e), reveal(e.target));
120
+ });
121
+ });
122
+ [...d.querySelectorAll('a.footnote-backref'), fn, tm].forEach(el => el?.remove());
123
+ const tms = d.querySelectorAll('span.timer'), t1 = 1000 * tms[0].dataset.total;
124
+ let t0;
125
+ function startTimers() {
126
+ t0 = new Date();
127
+ setInterval(setTimers, 1000);
128
+ }
129
+ function setTimers() {
130
+ if (!dC.contains('slide-mode')) return; // set timer only in slide mode
131
+ let t = (new Date() - t0);
132
+ if (t1) t = t1 - t;
133
+ const t2 = new Date(Math.abs(t)).toISOString().substr(11, 8).replace(/^00:/, '');
134
+ tms.forEach(el => {
135
+ el.innerText = t2;
136
+ if (t < 0) el.style.visibility = el.style.visibility === 'hidden' ? '' : 'hidden';
137
+ });
138
+ }
139
+ function toggleView(e) {
140
+ (dC.toggle('overview') ? dC.remove('slide-mode') : setScale(e));
141
+ }
142
+ // press f for fullscreen mode
143
+ d.addEventListener('keyup', e => {
144
+ if (e.target !== d.body) return;
145
+ e.key === 'f' && dE.requestFullscreen();
146
+ e.key === 'o' && toggleView(e);
147
+ e.key === 'm' && dC.toggle('mirrored');
148
+ sessionStorage.setItem('body-class', d.body.className);
149
+ });
150
+ // start timer and set scale on fullscreen
151
+ d.onfullscreenchange = e => {
152
+ d.fullscreenElement && (!t0 && startTimers(), setScale(e));
153
+ }
154
+ tms.forEach(el => el.addEventListener('click', e => startTimers()));
155
+ // measure the height of an empty slide
156
+ let H = -1;
157
+ function slideHeight() {
158
+ if (H >= 0) return H;
159
+ const s = newEl('div', 'slide');
160
+ p.querySelector('.slide:last-of-type').after(s);
161
+ H = s.offsetHeight;
162
+ s.remove()
163
+ return H;
164
+ }
165
+ // scale slides according to window height (baseline: 900px)
166
+ const sty = newEl('style'); sty.setAttribute('type', 'text/css');
167
+ d.head.append(sty);
168
+ // default height/width ratio from screen size
169
+ sty.innerHTML = `:root{--slide-ratio:${screen.height / screen.width}}`;
170
+ // read --slide-ratio in case users have set it in their CSS
171
+ const ratio = +getComputedStyle(dE).getPropertyValue('--slide-ratio');
172
+ function setScale(e) {
173
+ // navigate to a slide indicated by the hash if provided
174
+ e.type === 'load' && reveal(slides[location.hash.replace(/^#/, '') - 1]);
175
+ if (dC.contains('overview')) return;
176
+ let h = window.innerHeight, p = h / 900, r2 = h / window.innerWidth, full = d.fullscreenElement;
177
+ // add slide mode if there's enough window width, and remove it in case of scrollbar
178
+ dC.toggle('slide-mode', full || r2 <= ratio) &&
179
+ (!full && (dE.scrollWidth > dE.offsetWidth)) && dC.remove('slide-mode');
180
+ sty.innerHTML = `:root{--slide-ratio:${ratio};--slide-scale:${p};--slide-top:${(p - 1)/2 * d.body.scrollHeight + 'px'};}`;
181
+ // add spacers with enough height on load
182
+ !d.querySelector('.spacer.fade') && dC.contains('slide-mode') && slides.forEach(s => {
183
+ const sp = newEl('div', 'spacer fade'), h = s.offsetHeight, h2 = slideHeight();
184
+ s.append(sp);
185
+ if (h <= h2) return;
186
+ sp.style.height = (h2 - h % h2) * p + 'px';
187
+ });
188
+ }
189
+ ['load', 'resize'].forEach(evt => window.addEventListener(evt, setScale));
190
+ // restore previsouly saved body class
191
+ const bc = sessionStorage.getItem('body-class');
192
+ if (bc) d.body.className += ' ' + bc;
193
+ })(document);
package/js/snap.min.js ADDED
@@ -0,0 +1 @@
1
+ !function(e){let t=e.body;const n=":scope > h2";function o(e,n=1){if(t.querySelectorAll(e).length>=n)return!0;for(let o=0;o<t.children.length;o++)if(t.children[o].querySelectorAll(e).length>=n){t=t.children[o];break}return!1}function r(t,n){const o=e.createElement(t);return n&&(o.className=n),o}if(!o(":scope > hr:not([class])",3)&&"BODY"===t.tagName){if(!o(n)&&"BODY"===t.tagName)return;t.querySelectorAll(n).forEach((e=>e.before(r("hr"))))}function i(e){return""===e?.innerText?e:r("div","slide")}function s(e){return"HR"===e.tagName&&0===e.attributes.length}t.classList.add("slide-container"),[".frontmatter","#TOC"].forEach((t=>{const n=e.body.querySelector(t);if(n)if(".frontmatter"===t)n.classList.add("slide");else{const e=i();n.before(e),e.append(n)}}));let l=t.firstElementChild;if(s(l)&&l.remove(),l=t.firstElementChild,!l)return;let c=i();for(l.before(c);;){let e=c.nextSibling;if(!e)break;s(e)?(c=i(c),e.before(c),e.remove()):e.classList?.contains("slide")?(c=i(c),e.after(c)):c.append(e)}function a(e,t){const n=r("div");n.innerHTML=`<div ${t}></div>`;const o=n.firstElementChild.attributes;for(const t of o)e.setAttribute(t.name,t.value);n.remove()}function d(e){setTimeout((()=>e?.scrollIntoView()),100)}const f=e.documentElement,u=e.body.classList,h=e.querySelectorAll("div.slide"),m=h.length,p=e.querySelector("span.timer"),y=e.querySelector(".footnotes");h.forEach(((e,t)=>{const n=r("div","header"),o=r("div","main"),i=r("div","footer");o.append(...e.childNodes),e.append(o),e.insertAdjacentElement("afterbegin",n),e.insertAdjacentElement("beforeend",i),y&&e.querySelectorAll('.footnote-ref > a[href^="#fn"]').forEach((t=>{const n=y.querySelector("li"+t.getAttribute("href"));if(!n)return;let o=e.querySelector("section.footnotes");o||(o=r("section","footnotes"),i.before(o)),o.append(n),n.firstElementChild?.insertAdjacentHTML("afterbegin",`[${t.innerHTML}] `),n.outerHTML=n.innerHTML})),i.append(p?p.cloneNode():r("span","timer"));const s=r("span","page-number");s.innerText=t+1+"/"+m,s.onclick=e=>location.hash=t+1,i.append(s);for(const t of o.childNodes){if(t.nodeType!==Node.COMMENT_NODE)continue;let n=t.textContent;if(!/^#/.test(n))continue;n=n.replace(/^#/,"");const o=/[\s\n]class="([^"]+)"/,r=n.match(o);r&&(n=n.replace(o,"").trim(),e.className+=" "+r[1]),n&&a(e,n);break}e.addEventListener("click",(e=>{e.altKey&&(q(e),d(e.target))}))})),[...e.querySelectorAll("a.footnote-backref"),y,p].forEach((e=>e?.remove()));const g=e.querySelectorAll("span.timer"),b=1e3*g[0].dataset.total;let v;function E(){v=new Date,setInterval(S,1e3)}function S(){if(!u.contains("slide-mode"))return;let e=new Date-v;b&&(e=b-e);const t=new Date(Math.abs(e)).toISOString().substr(11,8).replace(/^00:/,"");g.forEach((n=>{n.innerText=t,e<0&&(n.style.visibility="hidden"===n.style.visibility?"":"hidden")}))}function q(e){u.toggle("overview")?u.remove("slide-mode"):N(e)}e.addEventListener("keyup",(t=>{t.target===e.body&&("f"===t.key&&f.requestFullscreen(),"o"===t.key&&q(t),"m"===t.key&&u.toggle("mirrored"),sessionStorage.setItem("body-class",e.body.className))})),e.onfullscreenchange=t=>{e.fullscreenElement&&(!v&&E(),N(t))},g.forEach((e=>e.addEventListener("click",(e=>E()))));let L=-1;function T(){if(L>=0)return L;const e=r("div","slide");return t.querySelector(".slide:last-of-type").after(e),L=e.offsetHeight,e.remove(),L}const w=r("style");w.setAttribute("type","text/css"),e.head.append(w),w.innerHTML=`:root{--slide-ratio:${screen.height/screen.width}}`;const A=+getComputedStyle(f).getPropertyValue("--slide-ratio");function N(t){if("load"===t.type&&d(h[location.hash.replace(/^#/,"")-1]),u.contains("overview"))return;let n=window.innerHeight,o=n/900,i=n/window.innerWidth,s=e.fullscreenElement;u.toggle("slide-mode",s||i<=A)&&!s&&f.scrollWidth>f.offsetWidth&&u.remove("slide-mode"),w.innerHTML=`:root{--slide-ratio:${A};--slide-scale:${o};--slide-top:${(o-1)/2*e.body.scrollHeight+"px"};}`,!e.querySelector(".spacer.fade")&&u.contains("slide-mode")&&h.forEach((e=>{const t=r("div","spacer fade"),n=e.offsetHeight,i=T();e.append(t),n<=i||(t.style.height=(i-n%i)*o+"px")}))}["load","resize"].forEach((e=>window.addEventListener(e,N)));const H=sessionStorage.getItem("body-class");H&&(e.body.className+=" "+H)}(document);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiee/utils",
3
- "version": "1.9.5",
3
+ "version": "1.10.0",
4
4
  "description": "Miscellaneous tools and utilities to manipulate HTML pages",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"