@xiee/utils 1.14.58 → 1.14.60
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/css/default.css +1 -1
- package/css/default.min.css +1 -1
- package/css/default.min.css.map +1 -1
- package/css/snap-clean.css +103 -0
- package/css/snap-clean.min.css +2 -0
- package/css/snap-clean.min.css.map +1 -0
- package/lt.css +57 -0
- package/lt.js +547 -0
- package/package.json +1 -1
package/css/default.css
CHANGED
package/css/default.min.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
body{-webkit-print-color-adjust:exact;print-color-adjust:exact;max-width:800px;margin:auto;padding:1em;font-family:sans-serif;line-height:1.5}body,.abstract,code,.footnotes,footer,#refs,.caption{font-size:.9em}li li{font-size:.95em}ul:has(li>input[type=checkbox]){padding-left:1em;list-style:none}*,:before,:after{box-sizing:border-box}a{color:#4682b4}pre,img{max-width:100%}pre{white-space:pre-wrap;word-break:break-word}pre code{padding:1em;display:block;overflow-x:auto}code{font-family:DejaVu Sans Mono,Droid Sans Mono,Lucida Console,Consolas,Monaco,monospace}:not(pre,th)>code,code[class],div>.caption{background:#f8f8f8}pre>code:is(:not([class]),.language-plain,.language-none,.plain),.box,.figure,.table{background:inherit;border:1px solid #eee}pre>code{&.message{border-color:#9eeaf9}&.warning{background:#fff3cd;border-color:#fff3cd}&.error{background:#f8d7da;border-color:#f8d7da}}.fenced-chunk{border-left:1px solid #666}.code-fence{opacity:.4;border:1px dashed #666;border-left:2px solid;&:hover{opacity:inherit}}.box,.figure,.table,table{margin:1em auto}div>.caption{padding:1px 1em}.figure{& p:has(img,svg),& pre:has(svg){text-align:center}}.flex-col{justify-content:space-between;display:flex}table{&:only-child:not(.table>*){margin:auto}& th,& td{font-variant-numeric:tabular-nums;padding:5px}&:not([class]){& thead,& tfoot,& tr:nth-child(2n){background:#f5f5f5}& thead th{border-bottom:1px solid #ddd}border-top:1px solid #666;border-bottom:1px solid #666}}blockquote{color:#666;border-left:.5em solid #eee;margin:0;padding:1px 1em}hr,.footnotes:before{border:1px dashed #ddd}.frontmatter{text-align:center}#TOC{& a{text-decoration:none}& ul{columns:24em;padding-left:1em;list-style:none}&>ul{padding:0}& ul ul{border-left:1px solid #b0c4de}}.body h2{border-bottom:1px solid #666}.body .appendix,.appendix~h2{border-bottom-style:dashed}.main-number:after{content:"."}span[class^=ref-number-]{font-weight:700}.ref-number-fig:after,.ref-number-tab:after{content:":"}.cross-ref-chp:before{content:"Chapter "}.cross-ref-sec:before{content:"Section "}.cross-ref-apd:before{content:"Appendix "}.cross-ref-fig:before,.ref-number-fig:before{content:"Figure "}.cross-ref-tab:before,.ref-number-tab:before{content:"Table "}.cross-ref-eqn:before,.MathJax_ref:has(mjx-mtext>mjx-c+mjx-c):before{content:"Equation "}.abstract,#refs{&:before{margin:1em auto;font-weight:700;display:block}}.abstract:before{content:"Abstract";text-align:center}#refs:before{content:"Bibliography";font-size:1.5em}.ref-paren-open:before{content:"("}.ref-paren-close:after{content:")"}.ref-semicolon:after{content:"; "}.ref-and:after{content:" and "}.ref-et-al:after{content:" et al.";font-style:italic}.footnote-ref a{&:before{content:"["}&:after{content:"]"}}section.footnotes{margin-top:2em;&:before{content:"";max-width:20em;display:block}}.fade{opacity:.6;background:repeating-linear-gradient(135deg,#fff,#fff 30px,#ddd 32px 32px)}@media (prefers-color-scheme:dark){html,img,video,iframe
|
|
1
|
+
body{-webkit-print-color-adjust:exact;print-color-adjust:exact;max-width:800px;margin:auto;padding:1em;font-family:sans-serif;line-height:1.5}body,.abstract,code,.footnotes,footer,#refs,.caption{font-size:.9em}li li{font-size:.95em}ul:has(li>input[type=checkbox]){padding-left:1em;list-style:none}*,:before,:after{box-sizing:border-box}a{color:#4682b4}pre,img{max-width:100%}pre{white-space:pre-wrap;word-break:break-word}pre code{padding:1em;display:block;overflow-x:auto}code{font-family:DejaVu Sans Mono,Droid Sans Mono,Lucida Console,Consolas,Monaco,monospace}:not(pre,th)>code,code[class],div>.caption{background:#f8f8f8}pre>code:is(:not([class]),.language-plain,.language-none,.plain),.box,.figure,.table{background:inherit;border:1px solid #eee}pre>code{&.message{border-color:#9eeaf9}&.warning{background:#fff3cd;border-color:#fff3cd}&.error{background:#f8d7da;border-color:#f8d7da}}.fenced-chunk{border-left:1px solid #666}.code-fence{opacity:.4;border:1px dashed #666;border-left:2px solid;&:hover{opacity:inherit}}.box,.figure,.table,table{margin:1em auto}div>.caption{padding:1px 1em}.figure{& p:has(img,svg),& pre:has(svg){text-align:center}}.flex-col{justify-content:space-between;display:flex}table{&:only-child:not(.table>*){margin:auto}& th,& td{font-variant-numeric:tabular-nums;padding:5px}&:not([class]){& thead,& tfoot,& tr:nth-child(2n){background:#f5f5f5}& thead th{border-bottom:1px solid #ddd}border-top:1px solid #666;border-bottom:1px solid #666}}blockquote{color:#666;border-left:.5em solid #eee;margin:0;padding:1px 1em}hr,.footnotes:before{border:1px dashed #ddd}.frontmatter{text-align:center}#TOC{& a{text-decoration:none}& ul{columns:24em;padding-left:1em;list-style:none}&>ul{padding:0}& ul ul{border-left:1px solid #b0c4de}}.body h2{border-bottom:1px solid #666}.body .appendix,.appendix~h2{border-bottom-style:dashed}.main-number:after{content:"."}span[class^=ref-number-]{font-weight:700}.ref-number-fig:after,.ref-number-tab:after{content:":"}.cross-ref-chp:before{content:"Chapter "}.cross-ref-sec:before{content:"Section "}.cross-ref-apd:before{content:"Appendix "}.cross-ref-fig:before,.ref-number-fig:before{content:"Figure "}.cross-ref-tab:before,.ref-number-tab:before{content:"Table "}.cross-ref-eqn:before,.MathJax_ref:has(mjx-mtext>mjx-c+mjx-c):before{content:"Equation "}.abstract,#refs{&:before{margin:1em auto;font-weight:700;display:block}}.abstract:before{content:"Abstract";text-align:center}#refs:before{content:"Bibliography";font-size:1.5em}.ref-paren-open:before{content:"("}.ref-paren-close:after{content:")"}.ref-semicolon:after{content:"; "}.ref-and:after{content:" and "}.ref-et-al:after{content:" et al.";font-style:italic}.footnote-ref a{&:before{content:"["}&:after{content:"]"}}section.footnotes{margin-top:2em;&:before{content:"";max-width:20em;display:block}}.fade{opacity:.6;background:repeating-linear-gradient(135deg,#fff,#fff 30px,#ddd 32px 32px)}@media (prefers-color-scheme:dark){html,img,video,iframe{filter:invert()}body{background:#ededed}}@media print{body{max-width:100%}tr,img{break-inside:avoid}}@media only screen and (width>=992px){body:not(.pagesjs) pre:has(.line-numbers):not(:hover){white-space:pre}}
|
|
2
2
|
/*# sourceMappingURL=default.min.css.map */
|
package/css/default.min.css.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"mappings":"AAAA,8IASA,oEACA,sBACA,iEACA,uCACA,gBACA,uBACA,+CACA,mDACA,2FACA,8DACA,8HACA,SACE,+BACA,kDACA,iDAEF,yCACA,oEAIE,yBAEF,0CACA,6BACA,QAAU,mDACV,qDACA,MACE,uCACA,wDACA,eACE,sDACA,wCACW,wDAIf,2EAMA,4CACA,+BACA,KACE,yBACA,mDACA,eACA,uCAEF,sCACA,wDACA,+BACA,yCACA,wDACA,yCACA,yCACA,0CACA,+DACA,8DACA,yFACA,gBACE,wDAEF,sDACA,oDACA,mCACA,mCACA,kCACA,+BACA,qDACA,gBACE,qBACA,qBAEF,iCAEE,kDAEF,4FAIA,mCACE,
|
|
1
|
+
{"version":3,"mappings":"AAAA,8IASA,oEACA,sBACA,iEACA,uCACA,gBACA,uBACA,+CACA,mDACA,2FACA,8DACA,8HACA,SACE,+BACA,kDACA,iDAEF,yCACA,oEAIE,yBAEF,0CACA,6BACA,QAAU,mDACV,qDACA,MACE,uCACA,wDACA,eACE,sDACA,wCACW,wDAIf,2EAMA,4CACA,+BACA,KACE,yBACA,mDACA,eACA,uCAEF,sCACA,wDACA,+BACA,yCACA,wDACA,yCACA,yCACA,0CACA,+DACA,8DACA,yFACA,gBACE,wDAEF,sDACA,oDACA,mCACA,mCACA,kCACA,+BACA,qDACA,gBACE,qBACA,qBAEF,iCAEE,kDAEF,4FAIA,mCACE,sCACA,yBAEF,aACE,oBACA,2BAEF,sCACE","sources":["default.css"],"sourcesContent":["body {\n font-family: sans-serif;\n max-width: 800px;\n margin: auto;\n padding: 1em;\n line-height: 1.5;\n print-color-adjust: exact;\n -webkit-print-color-adjust: exact;\n}\nbody, .abstract, code, .footnotes, footer, #refs, .caption { font-size: .9em; }\nli li { font-size: .95em; }\nul:has(li > input[type=\"checkbox\"]) { list-style: none; padding-left: 1em; }\n*, :before, :after { box-sizing: border-box; }\na { color: steelblue; }\npre, img { max-width: 100%; }\npre { white-space: pre-wrap; word-break: break-word; }\npre code { display: block; padding: 1em; overflow-x: auto; }\ncode { font-family: 'DejaVu Sans Mono', 'Droid Sans Mono', 'Lucida Console', Consolas, Monaco, monospace; }\n:not(pre, th) > code, code[class], div > .caption { background: #f8f8f8; }\npre > code:is(:not([class]), .language-plain, .language-none, .plain), .box, .figure, .table { background: inherit; border: 1px solid #eee; }\npre > code {\n &.message { border-color: #9eeaf9; }\n &.warning { background: #fff3cd; border-color: #fff3cd; }\n &.error { background: #f8d7da; border-color: #f8d7da; }\n}\n.fenced-chunk { border-left: 1px solid #666; }\n.code-fence {\n opacity: .4;\n border: 1px dashed #666;\n border-left: 2px solid;\n &:hover { opacity: inherit; }\n}\n.box, .figure, .table, table { margin: 1em auto; }\ndiv > .caption { padding: 1px 1em; }\n.figure { p:has(img, svg), pre:has(svg) { text-align: center; } }\n.flex-col { display: flex; justify-content: space-between; }\ntable {\n &:only-child:not(.table > *) { margin: auto; }\n th, td { padding: 5px; font-variant-numeric: tabular-nums; }\n &:not([class]) {\n thead, tfoot, tr:nth-child(even) { background: whitesmoke; }\n thead th { border-bottom: 1px solid #ddd; }\n border-top: 1px solid #666;\n border-bottom: 1px solid #666;\n }\n}\nblockquote {\n color: #666;\n margin: 0;\n padding: 1px 1em;\n border-left: .5em solid #eee;\n}\nhr, .footnotes::before { border: 1px dashed #ddd; }\n.frontmatter { text-align: center; }\n#TOC {\n a { text-decoration: none; }\n ul { list-style: none; padding-left: 1em; columns: 24em; }\n & > ul { padding: 0; }\n ul ul { border-left: 1px solid lightsteelblue; }\n}\n.body h2 { border-bottom: 1px solid #666; }\n.body .appendix, .appendix ~ h2 { border-bottom-style: dashed; }\n.main-number::after { content: \".\"; }\nspan[class^=\"ref-number-\"] { font-weight: bold; }\n.ref-number-fig::after, .ref-number-tab::after { content: \":\"; }\n.cross-ref-chp::before { content: \"Chapter \"; }\n.cross-ref-sec::before { content: \"Section \"; }\n.cross-ref-apd::before { content: \"Appendix \"; }\n.cross-ref-fig::before, .ref-number-fig::before { content: \"Figure \"; }\n.cross-ref-tab::before, .ref-number-tab::before { content: \"Table \"; }\n.cross-ref-eqn::before, .MathJax_ref:has(mjx-mtext > mjx-c + mjx-c)::before { content: \"Equation \"; }\n.abstract, #refs {\n &::before { display: block; margin: 1em auto; font-weight: bold; }\n}\n.abstract::before { content: \"Abstract\"; text-align: center; }\n#refs::before { content: \"Bibliography\"; font-size: 1.5em; }\n.ref-paren-open::before { content: \"(\"; }\n.ref-paren-close::after { content: \")\"; }\n.ref-semicolon::after { content: \"; \"; }\n.ref-and::after { content: \" and \"; }\n.ref-et-al::after { content: \" et al.\"; font-style: italic; }\n.footnote-ref a {\n &::before { content: \"[\"; }\n &::after { content: \"]\"; }\n}\nsection.footnotes {\n margin-top: 2em;\n &::before { content: \"\"; display: block; max-width: 20em; }\n}\n.fade {\n background: repeating-linear-gradient(135deg, white, white 30px, #ddd 32px, #ddd 32px);\n opacity: 0.6;\n}\n@media (prefers-color-scheme: dark) {\n html, img, video, iframe { filter: invert(1); }\n body { background: #ededed; }\n}\n@media print {\n body { max-width: 100%; }\n tr, img { break-inside: avoid; }\n}\n@media only screen and (min-width: 992px) {\n body:not(.pagesjs) pre:has(.line-numbers):not(:hover) { white-space: pre; }\n}\n"],"names":[]}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/* Clean theme for litedown slides (based on github.com/grantmcdermott/litedown-clean) */
|
|
2
|
+
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400&display=swap');
|
|
3
|
+
:root {
|
|
4
|
+
--clean-jet: #131516;
|
|
5
|
+
--clean-accent: #107895;
|
|
6
|
+
--clean-accent2: #9a2515;
|
|
7
|
+
--right-arrow: "\2192";
|
|
8
|
+
}
|
|
9
|
+
body {
|
|
10
|
+
font-family: "Roboto", sans-serif;
|
|
11
|
+
color: var(--clean-jet);
|
|
12
|
+
|
|
13
|
+
a { color: var(--clean-accent); line-height: 1.5em; }
|
|
14
|
+
p { margin-top: 1.25em; }
|
|
15
|
+
|
|
16
|
+
.slide, .frontmatter {
|
|
17
|
+
padding-left: 3em;
|
|
18
|
+
padding-right: 3em;
|
|
19
|
+
max-width: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.frontmatter {
|
|
23
|
+
text-align: left;
|
|
24
|
+
.title h1 {
|
|
25
|
+
color: var(--clean-jet);
|
|
26
|
+
margin-bottom: 0.1em;
|
|
27
|
+
}
|
|
28
|
+
.subtitle h2 {
|
|
29
|
+
color: var(--clean-accent);
|
|
30
|
+
font-style: italic;
|
|
31
|
+
margin-top: 0;
|
|
32
|
+
}
|
|
33
|
+
.author h2 {
|
|
34
|
+
color: var(--clean-jet);
|
|
35
|
+
margin-top: 1em;
|
|
36
|
+
}
|
|
37
|
+
.date h3 {
|
|
38
|
+
color: #666;
|
|
39
|
+
font-style: italic;
|
|
40
|
+
margin-top: 0.5em;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
h1, h2, h3, h4 { border-bottom: none; font-weight: lighter; }
|
|
45
|
+
h1 { color: var(--clean-jet); }
|
|
46
|
+
h2 { color: var(--clean-jet); margin-bottom: 0.1em; }
|
|
47
|
+
h3 {
|
|
48
|
+
color: var(--clean-accent);
|
|
49
|
+
font-style: italic;
|
|
50
|
+
margin-top: 0.25em;
|
|
51
|
+
}
|
|
52
|
+
h4 {
|
|
53
|
+
color: var(--clean-accent2);
|
|
54
|
+
margin-top: 1.25em;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.alert { color: var(--clean-accent2); }
|
|
58
|
+
.fg { color: var(--col, var(--clean-jet)); }
|
|
59
|
+
.bg {
|
|
60
|
+
background-color: var(--col, #fff);
|
|
61
|
+
padding: 0.1em;
|
|
62
|
+
border-radius: 5px;
|
|
63
|
+
display: inline-block;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ul {
|
|
67
|
+
padding-left: 1.5em;
|
|
68
|
+
li::marker { color: color-mix(in srgb, var(--clean-accent) 70%, white); }
|
|
69
|
+
}
|
|
70
|
+
ul ul {
|
|
71
|
+
list-style: none;
|
|
72
|
+
li::before {
|
|
73
|
+
content: var(--right-arrow);
|
|
74
|
+
color: color-mix(in srgb, var(--clean-accent) 60%, white);
|
|
75
|
+
display: inline-block;
|
|
76
|
+
width: 1em;
|
|
77
|
+
margin-left: -1em;
|
|
78
|
+
margin-right: 0.5em;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
ol {
|
|
82
|
+
padding-left: 16px;
|
|
83
|
+
li::marker { color: var(--clean-accent); }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.inverse { background-color: var(--clean-jet); color: white; }
|
|
87
|
+
|
|
88
|
+
::selection { background-color: #26351c; }
|
|
89
|
+
|
|
90
|
+
.columns3070, .columns7030, .columns4060, .columns6040, .columns5050 {
|
|
91
|
+
display: flex;
|
|
92
|
+
gap: min(4vw, 1.5em);
|
|
93
|
+
}
|
|
94
|
+
.columns5050 > div { width: 50%; }
|
|
95
|
+
.columns3070 > div:first-child { width: 30%; }
|
|
96
|
+
.columns3070 > div:not(:first-child) { width: 70%; }
|
|
97
|
+
.columns7030 > div:first-child { width: 70%; }
|
|
98
|
+
.columns7030 > div:not(:first-child) { width: 30%; }
|
|
99
|
+
.columns4060 > div:first-child { width: 40%; }
|
|
100
|
+
.columns4060 > div:not(:first-child) { width: 60%; }
|
|
101
|
+
.columns6040 > div:first-child { width: 60%; }
|
|
102
|
+
.columns6040 > div:not(:first-child) { width: 40%; }
|
|
103
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
@import "https://fonts.googleapis.com/css2?family=Roboto:wght@300;400&display=swap";:root{--clean-jet:#131516;--clean-accent:#107895;--clean-accent2:#9a2515;--right-arrow:"→"}body{color:var(--clean-jet);font-family:Roboto,sans-serif;& a{color:var(--clean-accent);line-height:1.5em}& p{margin-top:1.25em}& .slide,& .frontmatter{max-width:none;padding-left:3em;padding-right:3em}& .frontmatter{text-align:left;& .title h1{color:var(--clean-jet);margin-bottom:.1em}& .subtitle h2{color:var(--clean-accent);margin-top:0;font-style:italic}& .author h2{color:var(--clean-jet);margin-top:1em}& .date h3{color:#666;margin-top:.5em;font-style:italic}}& h1,& h2,& h3,& h4{border-bottom:none;font-weight:lighter}& h1{color:var(--clean-jet)}& h2{color:var(--clean-jet);margin-bottom:.1em}& h3{color:var(--clean-accent);margin-top:.25em;font-style:italic}& h4{color:var(--clean-accent2);margin-top:1.25em}& .alert{color:var(--clean-accent2)}& .fg{color:var(--col,var(--clean-jet))}& .bg{background-color:var(--col,#fff);border-radius:5px;padding:.1em;display:inline-block}& ul{padding-left:1.5em;& li::marker{color:color-mix(in srgb, var(--clean-accent) 70%, white)}}& ul ul{list-style:none;& li:before{content:var(--right-arrow);color:color-mix(in srgb, var(--clean-accent) 60%, white);width:1em;margin-left:-1em;margin-right:.5em;display:inline-block}}& ol{padding-left:16px;& li::marker{color:var(--clean-accent)}}& .inverse{background-color:var(--clean-jet);color:#fff}& ::selection{background-color:#26351c}& .columns3070,& .columns7030,& .columns4060,& .columns6040,& .columns5050{gap:min(4vw,1.5em);display:flex}& .columns5050>div{width:50%}& .columns3070>div:first-child{width:30%}& .columns3070>div:not(:first-child),& .columns7030>div:first-child{width:70%}& .columns7030>div:not(:first-child){width:30%}& .columns4060>div:first-child{width:40%}& .columns4060>div:not(:first-child),& .columns6040>div:first-child{width:60%}& .columns6040>div:not(:first-child){width:40%}}
|
|
2
|
+
/*# sourceMappingURL=snap-clean.min.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"mappings":"AACA,oFACA,6FAMA,0DAIE,gDACA,sBAEA,0EAMA,+BAEE,sDAIA,wEAKA,mDAIA,yDAOF,2DACA,4BACA,+CACA,kEAKA,kDAKA,oCACA,wCACA,2FAOA,wBAEE,uEAEF,wBAEE,mKASF,uBAEE,wCAGF,wDAEA,uCAEA,2GAIA,6BACA,yCACA,8EAEA,+CACA,yCACA,8EAEA","sources":["snap-clean.css"],"sourcesContent":["/* Clean theme for litedown slides (based on github.com/grantmcdermott/litedown-clean) */\n@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400&display=swap');\n:root {\n --clean-jet: #131516;\n --clean-accent: #107895;\n --clean-accent2: #9a2515;\n --right-arrow: \"\\2192\";\n}\nbody {\n font-family: \"Roboto\", sans-serif;\n color: var(--clean-jet);\n\n a { color: var(--clean-accent); line-height: 1.5em; }\n p { margin-top: 1.25em; }\n\n .slide, .frontmatter {\n padding-left: 3em;\n padding-right: 3em;\n max-width: none;\n }\n\n .frontmatter {\n text-align: left;\n .title h1 {\n color: var(--clean-jet);\n margin-bottom: 0.1em;\n }\n .subtitle h2 {\n color: var(--clean-accent);\n font-style: italic;\n margin-top: 0;\n }\n .author h2 {\n color: var(--clean-jet);\n margin-top: 1em;\n }\n .date h3 {\n color: #666;\n font-style: italic;\n margin-top: 0.5em;\n }\n }\n\n h1, h2, h3, h4 { border-bottom: none; font-weight: lighter; }\n h1 { color: var(--clean-jet); }\n h2 { color: var(--clean-jet); margin-bottom: 0.1em; }\n h3 {\n color: var(--clean-accent);\n font-style: italic;\n margin-top: 0.25em;\n }\n h4 {\n color: var(--clean-accent2);\n margin-top: 1.25em;\n }\n\n .alert { color: var(--clean-accent2); }\n .fg { color: var(--col, var(--clean-jet)); }\n .bg {\n background-color: var(--col, #fff);\n padding: 0.1em;\n border-radius: 5px;\n display: inline-block;\n }\n\n ul {\n padding-left: 1.5em;\n li::marker { color: color-mix(in srgb, var(--clean-accent) 70%, white); }\n }\n ul ul {\n list-style: none;\n li::before {\n content: var(--right-arrow);\n color: color-mix(in srgb, var(--clean-accent) 60%, white);\n display: inline-block;\n width: 1em;\n margin-left: -1em;\n margin-right: 0.5em;\n }\n }\n ol {\n padding-left: 16px;\n li::marker { color: var(--clean-accent); }\n }\n\n .inverse { background-color: var(--clean-jet); color: white; }\n\n ::selection { background-color: #26351c; }\n\n .columns3070, .columns7030, .columns4060, .columns6040, .columns5050 {\n display: flex;\n gap: min(4vw, 1.5em);\n }\n .columns5050 > div { width: 50%; }\n .columns3070 > div:first-child { width: 30%; }\n .columns3070 > div:not(:first-child) { width: 70%; }\n .columns7030 > div:first-child { width: 70%; }\n .columns7030 > div:not(:first-child) { width: 30%; }\n .columns4060 > div:first-child { width: 40%; }\n .columns4060 > div:not(:first-child) { width: 60%; }\n .columns6040 > div:first-child { width: 60%; }\n .columns6040 > div:not(:first-child) { width: 40%; }\n}\n"],"names":[]}
|
package/lt.css
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
th, td{ padding: 5px; }
|
|
2
|
+
.lt-table {
|
|
3
|
+
--bg: 4px solid transparent;
|
|
4
|
+
|
|
5
|
+
border-collapse: separate;
|
|
6
|
+
caption-side: top;
|
|
7
|
+
margin: 1em auto;
|
|
8
|
+
border-spacing: 0;
|
|
9
|
+
border-top: 1.5px solid #888;
|
|
10
|
+
border-bottom: 1.5px solid #888;
|
|
11
|
+
|
|
12
|
+
caption {
|
|
13
|
+
padding: .5em;
|
|
14
|
+
text-align: center;
|
|
15
|
+
.lt-title { font-size: 1.2em; }
|
|
16
|
+
.lt-subtitle { font-size: 0.9em; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
th { font-weight: normal; }
|
|
20
|
+
tbody {
|
|
21
|
+
:not(:last-child) :is(td, th) { border-bottom: 1px solid #ddd; }
|
|
22
|
+
.lt-row-group :is(td, th) { border-bottom: 1.5px solid #ddd; }
|
|
23
|
+
.lt-row-group { text-align: left; }
|
|
24
|
+
.lt-row-group { vertical-align: top; }
|
|
25
|
+
}
|
|
26
|
+
thead th {
|
|
27
|
+
border-bottom: 1px solid #888;
|
|
28
|
+
&:not(:first-child) { border-left: var(--bg); }
|
|
29
|
+
&:not(:last-child) { border-right: var(--bg); }
|
|
30
|
+
}
|
|
31
|
+
.lt-indent { padding-left: 1em; }
|
|
32
|
+
.lt-spanner-row {
|
|
33
|
+
.lt-spanner { border-bottom: 1px solid #bbb; text-align: center; }
|
|
34
|
+
.lt-spanner-empty { border-bottom: none; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.lt-footer {
|
|
38
|
+
font-size: 0.85em;
|
|
39
|
+
.lt-footnote:first-child td { border-top: 1px solid #aaa; }
|
|
40
|
+
.lt-source-note:first-child td { border-top: 1px dashed #aaa; }
|
|
41
|
+
.lt-source-note td { font-style: italic; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.al-r { text-align: right; }
|
|
45
|
+
.al-c { text-align: center; }
|
|
46
|
+
|
|
47
|
+
.lt-fnref {
|
|
48
|
+
color: #06c;
|
|
49
|
+
&::before { content: " ["; }
|
|
50
|
+
&::after { content: "]"; }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
.lt-raw [title]::after {
|
|
54
|
+
content: " (" attr(title) ")";
|
|
55
|
+
background: lightyellow;
|
|
56
|
+
font-size: 0.85em;
|
|
57
|
+
}
|
package/lt.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/* lt.js — build a semantic <table> from a JSON spec.
|
|
2
|
+
* Call LT.build(spec) from an inline <script> to render a table in place.
|
|
3
|
+
* One runtime per page renders any number of tables.
|
|
4
|
+
*/
|
|
5
|
+
(root => {
|
|
6
|
+
"use strict";
|
|
7
|
+
if (root.LT?.buildHtml) return; // duplicate inclusion is a no-op
|
|
8
|
+
|
|
9
|
+
// `[<]` (not `<`) avoids `</…` so this file is safe to inline in <script>.
|
|
10
|
+
const esc = s => String(s)
|
|
11
|
+
.replace(/&/g, "&").replace(/[<]/g, "<")
|
|
12
|
+
.replace(/>/g, ">").replace(/"/g, """);
|
|
13
|
+
const sup = i => `<sup class="lt-fnref">${i}</sup>`;
|
|
14
|
+
// Stringify, mapping null/undefined to "" (0 and false stringify normally).
|
|
15
|
+
const str = v => String(v ?? "");
|
|
16
|
+
|
|
17
|
+
// --- Number formatting ---
|
|
18
|
+
const isNum = v => typeof v === "number" && !isNaN(v);
|
|
19
|
+
// A column is "numeric" if its first non-null value is a number.
|
|
20
|
+
const numCol = col => col?.length && typeof col.find(v => v != null) === "number";
|
|
21
|
+
|
|
22
|
+
function fmtNumber(v, decimals, bigMark) {
|
|
23
|
+
if (!isNum(v)) return null;
|
|
24
|
+
let s = decimals != null ? v.toFixed(decimals) : String(v);
|
|
25
|
+
if (/^-0(\.0+)?$/.test(s)) s = s.slice(1);
|
|
26
|
+
if (bigMark) {
|
|
27
|
+
const parts = s.split(".");
|
|
28
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, bigMark);
|
|
29
|
+
s = parts.join(".");
|
|
30
|
+
}
|
|
31
|
+
if (s[0] === "-") s = "−" + s.slice(1);
|
|
32
|
+
return decimals != null || bigMark ? s : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Pattern merge ---
|
|
36
|
+
function mergeVals(vals, pattern) {
|
|
37
|
+
if (!pattern) return vals.filter(v => v !== "").join(" ");
|
|
38
|
+
let remaining = pattern, result = "";
|
|
39
|
+
while (remaining) {
|
|
40
|
+
const m = remaining.match(/<<(.*?)>>/);
|
|
41
|
+
if (!m) { result += subRefs(remaining, vals); break; }
|
|
42
|
+
if (m.index > 0) result += subRefs(remaining.slice(0, m.index), vals);
|
|
43
|
+
const block = m[1],
|
|
44
|
+
refs = [...block.matchAll(/\{(\d+)\}/g)].map(x => +x[1]);
|
|
45
|
+
if (!refs.length || refs.every(i => vals[i - 1] !== ""))
|
|
46
|
+
result += subRefs(block, vals);
|
|
47
|
+
remaining = remaining.slice(m.index + m[0].length);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function subRefs(s, vals) {
|
|
52
|
+
for (let i = 0; i < vals.length; i++)
|
|
53
|
+
s = s.split("{" + (i + 1) + "}").join(vals[i]);
|
|
54
|
+
return s;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Auto-format numeric columns ---
|
|
58
|
+
function autoFmt(spec, display, nRow) {
|
|
59
|
+
if (spec.auto_format === false) return;
|
|
60
|
+
const data = spec.data || {}, ops = spec.ops || [],
|
|
61
|
+
colNames = Object.keys(data);
|
|
62
|
+
|
|
63
|
+
const fmtCols = new Set();
|
|
64
|
+
for (const op of ops) {
|
|
65
|
+
if (op.type === "fmt_number") for (const c of (op.columns || colNames)) fmtCols.add(c);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const getLabel = c => {
|
|
69
|
+
for (const op of ops) {
|
|
70
|
+
if (op.type === "label" && op.labels?.[c] != null) return op.labels[c];
|
|
71
|
+
}
|
|
72
|
+
return c;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
for (const c of colNames) {
|
|
76
|
+
if (fmtCols.has(c)) continue;
|
|
77
|
+
const col = data[c];
|
|
78
|
+
if (!numCol(col)) continue;
|
|
79
|
+
|
|
80
|
+
const lbl = getLabel(c),
|
|
81
|
+
pct = /%|[ _](pct|percent)$/i.test(lbl);
|
|
82
|
+
|
|
83
|
+
if (/year/i.test(lbl) && col.every(v => v == null || /^\d{4}$/.test(String(v)))) continue;
|
|
84
|
+
|
|
85
|
+
// Determine decimal places: find max significant decimals across column,
|
|
86
|
+
// then cap dynamically based on the largest integer-part width (targeting
|
|
87
|
+
// ~4 total significant digits): e.g., values <1 get up to 4 decimals,
|
|
88
|
+
// 10-99 get 2, >=1000 get 0.
|
|
89
|
+
let maxInt = 0, n = 0;
|
|
90
|
+
for (const v of col) {
|
|
91
|
+
if (!isNum(v)) continue;
|
|
92
|
+
// Number of digits before the decimal point
|
|
93
|
+
const a = Math.abs(pct ? v * 100 : v),
|
|
94
|
+
intW = a < 1 ? 0 : Math.floor(Math.log10(a)) + 1;
|
|
95
|
+
if (intW > maxInt) maxInt = intW;
|
|
96
|
+
// Significant decimals (strip trailing zeros; adjust for pct *100)
|
|
97
|
+
const m = String(v).match(/\.(\d+)/);
|
|
98
|
+
if (!m) continue;
|
|
99
|
+
const d = m[1].replace(/0+$/, "").length - (pct ? 2 : 0);
|
|
100
|
+
if (d > n) n = d;
|
|
101
|
+
}
|
|
102
|
+
// Cap: 4 decimals max, minus integer width (so large numbers get fewer).
|
|
103
|
+
// n is already >= 0, so only the upper cap is needed.
|
|
104
|
+
n = Math.min(n, Math.max(4 - maxInt, 0));
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i < nRow; i++) {
|
|
107
|
+
const v = col[i];
|
|
108
|
+
if (!isNum(v)) continue;
|
|
109
|
+
const s = fmtNumber(pct ? v * 100 : v, n, " ");
|
|
110
|
+
if (s != null) display[c][i] = s + (pct ? "%" : "");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Apply ops to data columns, return display strings per column ---
|
|
116
|
+
function applyOps(spec) {
|
|
117
|
+
const data = spec.data || {},
|
|
118
|
+
ops = spec.ops || [],
|
|
119
|
+
colNames = Object.keys(data),
|
|
120
|
+
nRow = colNames.length ? data[colNames[0]].length : 0;
|
|
121
|
+
|
|
122
|
+
// Working copy: display[col][row] = string
|
|
123
|
+
const display = {};
|
|
124
|
+
for (const c of colNames) {
|
|
125
|
+
display[c] = data[c].map(str);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
autoFmt(spec, display, nRow);
|
|
129
|
+
|
|
130
|
+
// Run fn(col, row, rawValue) over each existing cell of the given columns.
|
|
131
|
+
const eachCell = (cols, fn) => {
|
|
132
|
+
for (const c of cols) {
|
|
133
|
+
if (!data[c]) continue;
|
|
134
|
+
for (let i = 0; i < nRow; i++) fn(c, i, data[c][i]);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
for (const op of ops) {
|
|
139
|
+
const cols = op.columns || colNames;
|
|
140
|
+
switch (op.type) {
|
|
141
|
+
case "fmt_number":
|
|
142
|
+
eachCell(cols, (c, i, raw) => {
|
|
143
|
+
if (!isNum(raw)) return;
|
|
144
|
+
const v = op.percent === true ? raw * 100 : raw,
|
|
145
|
+
psfx = op.percent ? "%" : "",
|
|
146
|
+
f = fmtNumber(v, op.decimals, op.big_mark ?? ""),
|
|
147
|
+
pfx = op.prefix || "", sfx = (op.suffix || "") + psfx;
|
|
148
|
+
if (f != null) display[c][i] = pfx + f + sfx;
|
|
149
|
+
else if (pfx || sfx) display[c][i] = pfx + String(v) + sfx;
|
|
150
|
+
});
|
|
151
|
+
break;
|
|
152
|
+
case "fmt_date":
|
|
153
|
+
eachCell(cols, (c, i, raw) => {
|
|
154
|
+
if (raw == null) return;
|
|
155
|
+
const d = raw instanceof Date ? raw : new Date(raw);
|
|
156
|
+
if (isNaN(d)) return;
|
|
157
|
+
display[c][i] = op.method ? d[op.method]() :
|
|
158
|
+
op.options ? d.toLocaleDateString(op.locale, op.options) :
|
|
159
|
+
d.toLocaleDateString(op.locale);
|
|
160
|
+
});
|
|
161
|
+
break;
|
|
162
|
+
case "sub":
|
|
163
|
+
eachCell(cols, (c, i, raw) => {
|
|
164
|
+
if (op.small != null && isNum(raw) && raw !== 0 &&
|
|
165
|
+
Math.abs(raw) < op.small) {
|
|
166
|
+
display[c][i] = op.small_text ?? ("<" + op.small);
|
|
167
|
+
} else if (op.zero != null && raw === 0) {
|
|
168
|
+
display[c][i] = op.zero;
|
|
169
|
+
} else if (op.missing != null && raw == null) {
|
|
170
|
+
display[c][i] = op.missing;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
break;
|
|
174
|
+
case "merge": {
|
|
175
|
+
const mCols = op.columns;
|
|
176
|
+
if (!mCols || mCols.length < 2) break;
|
|
177
|
+
const target = mCols[0];
|
|
178
|
+
for (let i = 0; i < nRow; i++) {
|
|
179
|
+
const vals = mCols.map(c => display[c]?.[i] ?? "");
|
|
180
|
+
display[target][i] = mergeVals(vals, op.pattern);
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { display, nRow };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- Resolve structural spec fields from ops + data ---
|
|
190
|
+
function resolveSpec(spec) {
|
|
191
|
+
const data = spec.data || {},
|
|
192
|
+
ops = spec.ops || [],
|
|
193
|
+
colNames = Object.keys(data),
|
|
194
|
+
nRow = colNames.length ? data[colNames[0]].length : 0,
|
|
195
|
+
// Run fn on each op of the given type, in document order.
|
|
196
|
+
onOp = (t, fn) => { for (const op of ops) if (op.type === t) fn(op); };
|
|
197
|
+
|
|
198
|
+
// row_group: array → rowspan mode; string → separator-row mode
|
|
199
|
+
let rowGroupSep = typeof spec.row_group === "string";
|
|
200
|
+
const rowGroupCols = Array.isArray(spec.row_group) ? spec.row_group
|
|
201
|
+
: (spec.row_group ? [spec.row_group] : []);
|
|
202
|
+
if (!rowGroupSep && rowGroupCols.length === 1 && spec.auto_sep !== false &&
|
|
203
|
+
!numCol(data[rowGroupCols[0]]) &&
|
|
204
|
+
data[rowGroupCols[0]]?.some(v => (v + "").length > 20)) rowGroupSep = true;
|
|
205
|
+
|
|
206
|
+
// Hidden columns: row_group, merge sources
|
|
207
|
+
const hidden = new Set();
|
|
208
|
+
for (const g of rowGroupCols) hidden.add(g);
|
|
209
|
+
onOp("merge", op => {
|
|
210
|
+
if (op.hide !== false && op.columns)
|
|
211
|
+
op.columns.slice(1).forEach(c => hidden.add(c));
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Visible columns after hiding
|
|
215
|
+
let visible = colNames.filter(c => !hidden.has(c));
|
|
216
|
+
|
|
217
|
+
// Apply move
|
|
218
|
+
onOp("move", op => {
|
|
219
|
+
const toMove = (op.columns || []).filter(c => visible.includes(c));
|
|
220
|
+
if (!toMove.length) return;
|
|
221
|
+
const rest = visible.filter(c => !toMove.includes(c));
|
|
222
|
+
if (op.after == null) {
|
|
223
|
+
visible = [...toMove, ...rest];
|
|
224
|
+
} else {
|
|
225
|
+
const pos = rest.indexOf(op.after);
|
|
226
|
+
if (pos >= 0) visible = [...rest.slice(0, pos + 1), ...toMove, ...rest.slice(pos + 1)];
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Set arr[i] = val where i is the position of column c in `visible`.
|
|
231
|
+
const setByCol = (arr, c, val) => { const i = visible.indexOf(c); if (i >= 0) arr[i] = val; };
|
|
232
|
+
|
|
233
|
+
// Alignment: default from data type (number→right, else left), then overrides
|
|
234
|
+
const align = visible.map(c => numCol(data[c]) ? "right" : "left");
|
|
235
|
+
onOp("align", op => { for (const c of (op.columns || [])) setByCol(align, c, op.align); });
|
|
236
|
+
|
|
237
|
+
// Column labels
|
|
238
|
+
const autoLbl = s => spec.auto_label === false ? s : s.replace(/[._]/g, " ");
|
|
239
|
+
const colLabels = visible.map(autoLbl);
|
|
240
|
+
onOp("label", op => {
|
|
241
|
+
for (const [c, lbl] of Object.entries(op.labels || {})) setByCol(colLabels, c, lbl);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Column widths
|
|
245
|
+
let colWidths = null;
|
|
246
|
+
onOp("width", op => {
|
|
247
|
+
if (!op.widths) return;
|
|
248
|
+
if (!colWidths) colWidths = visible.map(() => "");
|
|
249
|
+
for (const [c, w] of Object.entries(op.widths)) setByCol(colWidths, c, w);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Indent
|
|
253
|
+
const indent = new Array(nRow).fill(0);
|
|
254
|
+
onOp("indent", op => {
|
|
255
|
+
if (op.rows) for (const r of op.rows) indent[r - 1] = op.level ?? 1;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Runs of equal consecutive values in col → [{ label, rows (1-based) }].
|
|
259
|
+
// Used for both separator-row groups and rowspan span sizes.
|
|
260
|
+
const runs = col => {
|
|
261
|
+
const out = [];
|
|
262
|
+
for (let i = 0; i < nRow;) {
|
|
263
|
+
const label = str(col[i]), rows = [i + 1];
|
|
264
|
+
while (++i < nRow && str(col[i]) === label) rows.push(i + 1);
|
|
265
|
+
out.push({ label, rows });
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Row groups from data column (separator-row mode only)
|
|
271
|
+
let groups = [];
|
|
272
|
+
if (rowGroupSep && rowGroupCols.length && data[rowGroupCols[0]])
|
|
273
|
+
groups.push(...runs(data[rowGroupCols[0]]));
|
|
274
|
+
// Manual groups
|
|
275
|
+
onOp("row_group", op => groups.push({ label: op.label, rows: op.rows }));
|
|
276
|
+
// Group ordering
|
|
277
|
+
onOp("group_order", op => {
|
|
278
|
+
if (!groups.length) return;
|
|
279
|
+
const ordered = [], rest = [];
|
|
280
|
+
for (const lbl of (op.order || [])) {
|
|
281
|
+
const g = groups.find(x => x.label === lbl);
|
|
282
|
+
if (g) ordered.push(g);
|
|
283
|
+
}
|
|
284
|
+
for (const g of groups) if (!ordered.includes(g)) rest.push(g);
|
|
285
|
+
groups = [...ordered, ...rest];
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Rowspan groups: compute span sizes for each group column
|
|
289
|
+
const rowSpans = [];
|
|
290
|
+
if (!rowGroupSep && rowGroupCols.length) {
|
|
291
|
+
for (const gc of rowGroupCols) {
|
|
292
|
+
const spans = new Array(nRow).fill(0);
|
|
293
|
+
for (const r of runs(data[gc] || [])) spans[r.rows[0] - 1] = r.rows.length;
|
|
294
|
+
// Resolve display label from label ops (last wins)
|
|
295
|
+
let hdr = gc;
|
|
296
|
+
onOp("label", op => { if (op.labels?.[gc] != null) hdr = op.labels[gc]; });
|
|
297
|
+
rowSpans.push({ col: gc, label: hdr, spans });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Styles: the style ops are consumed directly (css/class/columns/rows/test).
|
|
302
|
+
const styles = ops.filter(op => op.type === "style");
|
|
303
|
+
|
|
304
|
+
// Auto-spanners: split column names on separator, group contiguous prefixes
|
|
305
|
+
let spanners = spec.spanners || [];
|
|
306
|
+
if (spec.auto_span) {
|
|
307
|
+
const sep = typeof spec.auto_span === "string" ? new RegExp(spec.auto_span) : /[._]/;
|
|
308
|
+
spanners = [...spanners];
|
|
309
|
+
for (let i = 0; i < visible.length;) {
|
|
310
|
+
const m = visible[i].match(sep);
|
|
311
|
+
if (!m) { i++; continue; }
|
|
312
|
+
const prefix = visible[i].slice(0, m.index);
|
|
313
|
+
let j = i + 1;
|
|
314
|
+
while (j < visible.length) {
|
|
315
|
+
const m2 = visible[j].match(sep);
|
|
316
|
+
if (!m2 || visible[j].slice(0, m2.index) !== prefix) break;
|
|
317
|
+
j++;
|
|
318
|
+
}
|
|
319
|
+
if (j - i >= 2) {
|
|
320
|
+
spanners.push({ label: prefix, columns: visible.slice(i, j) });
|
|
321
|
+
for (let k = i; k < j; k++) {
|
|
322
|
+
const mk = visible[k].match(sep);
|
|
323
|
+
if (mk) colLabels[k] = autoLbl(visible[k].slice(mk.index + mk[0].length));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
i = j;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
visible, align, colLabels, colWidths, indent,
|
|
332
|
+
groups, rowSpans, styles, spanners,
|
|
333
|
+
footnotes: spec.footnotes || [],
|
|
334
|
+
notes: spec.notes || [],
|
|
335
|
+
header: spec.header || {},
|
|
336
|
+
nRow
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Footnotes: dedup by text, assign 1..N in first-seen order.
|
|
341
|
+
function indexFootnotes(fns) {
|
|
342
|
+
const order = [], idx = {};
|
|
343
|
+
fns.forEach(f => {
|
|
344
|
+
if (idx[f.text] == null) { idx[f.text] = order.length + 1; order.push(f.text); }
|
|
345
|
+
});
|
|
346
|
+
return { order, idx };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Find the footnote index for a (type, value) location; 0 if none.
|
|
350
|
+
// `val` is a scalar: the title group, a single column, a spanner label,
|
|
351
|
+
// or a row-group label, depending on `type`.
|
|
352
|
+
const matcher = (fns, idx) => (type, val) => {
|
|
353
|
+
for (const f of fns) {
|
|
354
|
+
const loc = f.location;
|
|
355
|
+
if (loc.type !== type) continue;
|
|
356
|
+
const hit = idx[f.text];
|
|
357
|
+
if (type === "title" && loc.group === val) return hit;
|
|
358
|
+
if (type === "column_labels" && loc.columns.includes(val)) return hit;
|
|
359
|
+
if (type === "column_spanners" && loc.spanners.includes(val)) return hit;
|
|
360
|
+
if (type === "row_groups") {
|
|
361
|
+
if (loc.match === "all") return hit;
|
|
362
|
+
if (loc.match === "exact" && loc.values.includes(val)) return hit;
|
|
363
|
+
if (loc.match === "starts_with" && val.startsWith(loc.value)) return hit;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return 0;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
function sortByGroups(spec) {
|
|
370
|
+
if (spec.sort === false || !spec.row_group) return;
|
|
371
|
+
const data = spec.data || {},
|
|
372
|
+
cols = [].concat(spec.row_group),
|
|
373
|
+
colNames = Object.keys(data),
|
|
374
|
+
nRow = colNames.length ? data[colNames[0]].length : 0;
|
|
375
|
+
if (!nRow) return;
|
|
376
|
+
const idx = Array.from({length: nRow}, (_, i) => i);
|
|
377
|
+
idx.sort((a, b) => {
|
|
378
|
+
for (const c of cols) {
|
|
379
|
+
const va = data[c]?.[a], vb = data[c]?.[b];
|
|
380
|
+
if (va == vb) continue;
|
|
381
|
+
if (va == null) return 1;
|
|
382
|
+
if (vb == null) return -1;
|
|
383
|
+
if (va < vb) return -1;
|
|
384
|
+
if (va > vb) return 1;
|
|
385
|
+
}
|
|
386
|
+
return 0;
|
|
387
|
+
});
|
|
388
|
+
for (const c of colNames) data[c] = idx.map(i => data[c][i]);
|
|
389
|
+
// Remap 1-based row indices in ops to reflect the new order
|
|
390
|
+
const newPos = new Array(nRow);
|
|
391
|
+
for (let i = 0; i < nRow; i++) newPos[idx[i]] = i + 1;
|
|
392
|
+
for (const op of (spec.ops || [])) {
|
|
393
|
+
if (op.rows) op.rows = op.rows.map(r => newPos[r - 1]);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function buildHtml(spec) {
|
|
398
|
+
sortByGroups(spec);
|
|
399
|
+
const data = spec.data || {},
|
|
400
|
+
{ display, nRow } = applyOps(spec),
|
|
401
|
+
{ visible: cols, align, colLabels, colWidths, indent,
|
|
402
|
+
groups, rowSpans, styles, spanners, footnotes: fns, notes, header: hdr } = resolveSpec(spec),
|
|
403
|
+
reg = indexFootnotes(fns),
|
|
404
|
+
fIdx = matcher(fns, reg.idx),
|
|
405
|
+
nGrp = rowSpans.length,
|
|
406
|
+
nCol = cols.length + nGrp,
|
|
407
|
+
out = [`<table class="lt-table">`];
|
|
408
|
+
const mark = (type, val) => { const i = fIdx(type, val); return i ? sup(i) : ""; };
|
|
409
|
+
const cell = (c, r) => display[c]?.[r - 1] ?? "";
|
|
410
|
+
// ` name="val"` for a truthy val, else "" — for optional HTML attributes.
|
|
411
|
+
const attr = (n, v) => v ? ` ${n}="${v}"` : "";
|
|
412
|
+
// Plain class names per column (alignment + leading-indent), "" if none.
|
|
413
|
+
const colCls = cols.map((_, i) => (
|
|
414
|
+
(i === 0 && groups.length ? "lt-indent " : "") +
|
|
415
|
+
({right: "al-r", center: "al-c"}[align[i]] || "")
|
|
416
|
+
).trimEnd());
|
|
417
|
+
|
|
418
|
+
// Visit each cell at (rows × cols) that exists in the table, calling
|
|
419
|
+
// fn(key, c, r). rows null = all rows; cols falls back to all columns.
|
|
420
|
+
const eachLoc = (rows, locCols, fn) => {
|
|
421
|
+
for (let r = 1; r <= nRow; r++) {
|
|
422
|
+
if (rows && !rows.includes(r)) continue;
|
|
423
|
+
for (const c of (locCols || cols)) {
|
|
424
|
+
const ci = cols.indexOf(c);
|
|
425
|
+
if (ci >= 0) fn(`${r},${ci}`, c, r);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// Style map: accumulate css (";") and class (" ") per cell.
|
|
431
|
+
const styleMap = {}, classMap = {};
|
|
432
|
+
const addTo = (map, key, val, sep) => { map[key] = map[key] ? map[key] + sep + val : val; };
|
|
433
|
+
for (const s of styles)
|
|
434
|
+
eachLoc(s.rows, s.columns ? [].concat(s.columns) : null, (key, c, r) => {
|
|
435
|
+
if (s.test && !s.test(data[c][r - 1])) return;
|
|
436
|
+
if (s.css) addTo(styleMap, key, s.css, ";");
|
|
437
|
+
if (s.class) addTo(classMap, key, s.class, " ");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// <colgroup>
|
|
441
|
+
if (colWidths && colWidths.some(w => w)) {
|
|
442
|
+
out.push(`<colgroup>`);
|
|
443
|
+
for (let i = 0; i < cols.length; i++)
|
|
444
|
+
out.push(`<col${attr("style", colWidths[i] ? "width:" + colWidths[i] : "")}>`);
|
|
445
|
+
out.push(`</colgroup>`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// <caption>: title + subtitle, each a <div> rendered only when non-empty.
|
|
449
|
+
const caption = [["title", "lt-title"], ["subtitle", "lt-subtitle"]]
|
|
450
|
+
.filter(([g]) => hdr[g] != null && hdr[g] !== "")
|
|
451
|
+
.map(([g, cls]) => `<div class="${cls}">${esc(hdr[g])}${mark("title", g)}</div>`);
|
|
452
|
+
if (caption.length)
|
|
453
|
+
out.push(`<caption class="lt-caption">`, ...caption, `</caption>`);
|
|
454
|
+
|
|
455
|
+
// <thead>
|
|
456
|
+
out.push(`<thead>`);
|
|
457
|
+
if (spanners.length) {
|
|
458
|
+
const emptyTh = `<th class="lt-spanner-empty"></th>`;
|
|
459
|
+
out.push(`<tr class="lt-spanner-row">`);
|
|
460
|
+
out.push(emptyTh.repeat(nGrp));
|
|
461
|
+
for (let k = 0; k < cols.length;) {
|
|
462
|
+
const sp = spanners.find(s => s.columns[0] === cols[k]);
|
|
463
|
+
if (sp) {
|
|
464
|
+
out.push(`<th colspan="${sp.columns.length}" scope="colgroup" class="lt-spanner">${esc(sp.label)}${mark("column_spanners", sp.label)}</th>`);
|
|
465
|
+
k += sp.columns.length;
|
|
466
|
+
} else {
|
|
467
|
+
out.push(emptyTh);
|
|
468
|
+
k++;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
out.push(`</tr>`);
|
|
472
|
+
}
|
|
473
|
+
out.push(`<tr>`);
|
|
474
|
+
for (const rs of rowSpans) out.push(`<th scope="col">${esc(rs.label)}</th>`);
|
|
475
|
+
for (let i = 0; i < cols.length; i++)
|
|
476
|
+
out.push(`<th scope="col"${attr("class", colCls[i])}>${esc(colLabels[i])}${mark("column_labels", cols[i])}</th>`);
|
|
477
|
+
out.push(`</tr></thead>`);
|
|
478
|
+
|
|
479
|
+
// Body footnote markers
|
|
480
|
+
const bodyMarks = {};
|
|
481
|
+
for (const f of fns) {
|
|
482
|
+
if (f.location.type !== "body") continue;
|
|
483
|
+
const fi = reg.idx[f.text];
|
|
484
|
+
eachLoc(f.location.rows, f.location.columns || [], key => { bodyMarks[key] = fi; });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Row rendering
|
|
488
|
+
const pushRow = r => {
|
|
489
|
+
out.push(`<tr>`);
|
|
490
|
+
// Rowspan group cells
|
|
491
|
+
for (const rs of rowSpans) {
|
|
492
|
+
const span = rs.spans[r - 1];
|
|
493
|
+
if (span > 0) out.push(`<th scope="row" class="lt-row-group"${attr("rowspan", span > 1 ? span : 0)}>${esc(cell(rs.col, r))}</th>`);
|
|
494
|
+
}
|
|
495
|
+
const ind = indent[r - 1] || 0;
|
|
496
|
+
for (let ci = 0; ci < cols.length; ci++) {
|
|
497
|
+
const k = `${r},${ci}`, c = cols[ci],
|
|
498
|
+
m = bodyMarks[k], cc = classMap[k],
|
|
499
|
+
cls = [colCls[ci], cc].filter(Boolean).join(" ");
|
|
500
|
+
let s = styleMap[k] || "";
|
|
501
|
+
if (ci === 0 && ind) s = (s ? s + ";" : "") + `padding-left:${ind + 1}em`;
|
|
502
|
+
const raw = str(data[c][r - 1]), disp = display[c][r - 1],
|
|
503
|
+
tip = raw !== disp ? ` title="${esc(raw)}"` : "";
|
|
504
|
+
out.push(`<td${attr("class", cls)}${attr("style", s)}${tip}>${esc(disp)}${m ? sup(m) : ""}</td>`);
|
|
505
|
+
}
|
|
506
|
+
out.push(`</tr>`);
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// <tbody>
|
|
510
|
+
out.push(`<tbody>`);
|
|
511
|
+
if (groups.length) {
|
|
512
|
+
const seen = {};
|
|
513
|
+
for (const g of groups) {
|
|
514
|
+
out.push(`<tr class="lt-row-group"><th colspan="${nCol}" scope="colgroup">${esc(g.label)}${mark("row_groups", g.label)}</th></tr>`);
|
|
515
|
+
for (const r of g.rows) { seen[r] = 1; pushRow(r); }
|
|
516
|
+
}
|
|
517
|
+
for (let r = 1; r <= nRow; r++) if (!seen[r]) pushRow(r);
|
|
518
|
+
} else {
|
|
519
|
+
for (let r = 1; r <= nRow; r++) pushRow(r);
|
|
520
|
+
}
|
|
521
|
+
out.push(`</tbody>`);
|
|
522
|
+
|
|
523
|
+
// <tfoot>: footnotes then source notes, each a full-width row.
|
|
524
|
+
if (reg.order.length || notes.length) {
|
|
525
|
+
const footRow = (cls, html) => `<tr class="${cls}"><td colspan="${nCol}">${html}</td></tr>`;
|
|
526
|
+
out.push(`<tfoot class="lt-footer">`);
|
|
527
|
+
reg.order.forEach((txt, i) => out.push(footRow("lt-footnote", `${sup(i + 1)} ${esc(txt)}`)));
|
|
528
|
+
for (const n of notes) out.push(footRow("lt-source-note", esc(n)));
|
|
529
|
+
out.push(`</tfoot>`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
out.push(`</table>`);
|
|
533
|
+
return out.join("");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const mount = (s, spec) => {
|
|
537
|
+
s.insertAdjacentHTML("afterend", buildHtml(spec));
|
|
538
|
+
const tbl = s.nextElementSibling,
|
|
539
|
+
raw = (e, el) => e.altKey && el.classList.toggle("lt-raw");
|
|
540
|
+
tbl.onclick = e => e.detail === 1 && raw(e, tbl);
|
|
541
|
+
tbl.ondblclick = e => raw(e, tbl.ownerDocument.documentElement);
|
|
542
|
+
};
|
|
543
|
+
// q.push renders immediately; replay any entries queued before we loaded.
|
|
544
|
+
const q = { push: e => mount(e.s, e.d) };
|
|
545
|
+
(root.LT?.q || []).forEach(q.push);
|
|
546
|
+
root.LT = { build: spec => mount(document.currentScript, spec), buildHtml, q };
|
|
547
|
+
})(window);
|